<template>
    <view class="container">
      <!-- 返回按鈕 -->
      <BackButton />
      
      <!-- 防作弊提示弹窗 -->
      <view class="modal" v-if="showCheatModal">
        <view class="modal-content">
          <h2>防作弊提示</h2>
          <p>请找一个光线好的地方，能清楚看到你的人脸。画面中最好不要有太多其他杂物(纯色背景墙面最好)，为排除作弊嫌疑，请视线盯者屏幕或摄像头(可以闭眼眨眼休息)，瞳孔不能看其他地方。不要翻看任何电子设备，边不能有其他声音提示，中途不能退出。如果你退出会重新生成题目。AI会提问，不需要打字，只要口头叙述回答即可。</p>
          <!-- 直接显示开始考试按钮，无需勾选同意 -->
          <button @click="startExam" class="start-btn">开始考试</button>
        </view>
      </view>
      
      <!-- 考试内容区域 -->
      <div v-if="showExamContent" class="exam-content">
        <!-- 计时器 -->
        <div class="timer">
          <text>剩余时间: {{ remainingTime }} 分钟</text>
        </div>
        
        <!-- 视频容器 -->
        <!-- #ifdef H5 -->
        <div id="video-container" class="video-container"></div>
        <!-- #endif -->
        
        <!-- 微信小程序端摄像头 -->
        <!-- #ifdef APP-PLUS || MP-WEIXIN -->
        <!-- 使用camera组件替代live-pusher，参考login页面实现 -->
        <camera
          id="videoCamera"
          device-position="front"
          flash="off"
          style="width: 100%; height: 300rpx;"
          @error="onCameraError"
        ></camera>
        
        <!-- 用于处理视频帧的canvas -->
        <canvas
          type="2d"
          id="videoCanvas"
          style="position: absolute; left: -9999px;"
          :width="canvasWidth"
          :height="canvasHeight"
        ></canvas>
        <!-- #endif -->
        
        <!-- 当前问题 -->
        <div class="question-container">
          <h3>问题 {{ currentQuestionIndex + 1 }}/{{ totalQuestions }}</h3>
          <p class="question-text">{{ currentQuestion?.questionText }}</p>
        </div>
        
        <!-- 控制按钮 -->
        <button @click="toggleCamera" class="action-btn">切换摄像头</button>
        <button @click="answerComplete" class="answer-btn">回答完毕</button>
        <button @click="testWhisperAudio" class="action-btn" :style="{ backgroundColor: whisperTestEnabled ? '#4CAF50' : '#ff9800' }">
          {{ whisperTestEnabled ? 'Whisper音频测试已开启' : 'Whisper音频测试' }}
        </button>
        <button @click="testAudioFormat" class="action-btn" style="background-color: #9C27B0;">
          测试音频格式
        </button>
        <button @click="testInt16Conversion" class="action-btn" style="background-color: #FF5722;">
          测试Int16转换
        </button>
        
        <!-- 状态信息 -->
        <view class="status">
          <text>视频状态: {{ videoStatus }}</text>
          <text>音频状态: {{ audioStatus }}</text>
        </view>
        
        <!-- 麦克风音量显示 -->
        <view class="volume-display">
          <view class="volume-label">麦克风音量</view>
          <view class="volume-bar-container">
            <view class="volume-bar" :style="{ width: volumePercentage + '%' }"></view>
          </view>
          <view class="volume-text">{{ volumeLevel }}%</view>
          <view class="volume-info" v-if="maxVolumeSeen > 0">
            <text class="volume-info-text">最大: {{ Math.round(maxVolumeSeen) }}%</text>
            <text class="volume-info-text" v-if="volumeHistory.length > 0">
              平均: {{ Math.round(volumeHistory.reduce((a, b) => a + b, 0) / volumeHistory.length) }}%
            </text>
          </view>
        </view>

        <!-- 识别结果显示面板 -->
        <view class="recognition-panel">
          <view class="recognition-header">
            <text class="recognition-title">语音识别结果</text>
            <button class="action-btn" style="padding: 6px 12px; margin: 0;" @click="clearRecognition">清空</button>
          </view>
          <view class="recognition-body">
            <pre class="recognition-text">{{ recognizedTextLog }}</pre>
          </view>
        </view>
        
        
        <!-- 考试结束提示 -->
        <view class="modal" v-if="showExamCompleteModal">
          <view class="modal-content exam-result-modal">
            <h2>考试结果</h2>
            
            <!-- 加载中状态 -->
            <view v-if="loadingResult" class="loading-container">
              <text>正在计算您的分数...</text>
            </view>
            
            <!-- 考试结果详情 -->
            <view v-else-if="examResult" class="result-container">
              <!-- 总分显示 -->
              <view class="score-section">
                <view class="total-score">
                  <text class="score-label">总分</text>
                  <text class="score-value">{{ examResult.averageScore }}/100</text>
                </view>
                <view class="score-details">
                  <text>答题数：{{ examResult.validQuestions }}/{{ examResult.totalQuestions }}</text>
                </view>
              </view>
              
              <!-- 题目详情 -->
              <view class="questions-section">
                <h3>题目详情</h3>
                <view v-for="(question, index) in examResult.questionResults" :key="question.questionId" class="question-item">
                  <view class="question-header">
                    <text class="question-number">第{{ index + 1 }}题</text>
                    <text class="question-score">{{ question.score }}分</text>
                  </view>
                  <view class="question-content">
                    <text class="question-text">{{ question.questionText }}</text>
                  </view>
                  <view v-if="question.modelAnswer" class="model-answer">
                    <text class="answer-label">评分原因：</text>
                    <text class="answer-text">{{ question.modelAnswer }}</text>
                  </view>
                </view>
              </view>
              
              <!-- 铭牌信息 -->
              <view v-if="examResult.badgeInfo && examResult.badgeInfo.length > 0" class="badges-section">
                <h3>相关铭牌</h3>
                <view v-for="badge in examResult.badgeInfo" :key="badge.id" class="badge-item" @click="goToTaskDetail(badge.taskId)">
                  <view class="badge-header">
                    <text class="badge-name">{{ badge.association }}</text>
                    <text class="badge-level">Lv.{{ badge.level }}</text>
                  </view>
                  <view v-if="badge.evaluation" class="badge-evaluation">
                    <text>{{ badge.evaluation }}</text>
                  </view>
                  <view v-if="badge.contributorScore !== undefined" class="badge-score">
                    <text>贡献分数：{{ badge.contributorScore }}</text>
                  </view>
                </view>
              </view>
            </view>
            
            <!-- 异常状态 -->
            <view v-else-if="examAbnormal" class="abnormal-container">
              <text class="abnormal-text">考试异常结束</text>
              <text class="abnormal-desc">由于检测到异常行为，考试已终止。</text>
            </view>
            
            <!-- 默认状态 -->
            <view v-else class="default-container">
              <text>感谢您完成本次考核，您的回答已提交。</text>
            </view>
            
            <button @click="navigateBack" class="complete-btn">确定</button>
          </view>
        </view>
      </div>
    </view>
  </template>
  
  <script>
import { NETWORK_CONFIG } from '@/config/network.js'
  import BackButton from '@/components/BackButton.vue'
  
  const FPS = 30; // 30fps 提升刷新率
  const AUDIO_SAMPLE_RATE = 16000; // 16kHz采样率
  const MAX_QUESTION_TIME = 10; // 每个问题最大回答时间(分钟)
  
  export default {
    data() {
      return {
        videoStream: null,
        videoFacingMode: 'user',
        videoElement: null,
        audioStream: null,
        ws: null,
        videoStatus: '未启动',
        audioStatus: '未启动',
        canvas: null,
        ctx: null,
        audioContext: null,
        processor: null,
        recorderManager: null,
        sendInterval: null,
        isConnected: false,
        reconnectAttempts: 0,
        useLivePusher: false, // 关闭live-pusher优先策略，统一走camera/takePhoto
        maxReconnectAttempts: 5,
        // 帧计数器，用于控制日志输出频率
        frameCounter: 0,
        // Canvas尺寸，用于小程序环境下视频帧处理
        canvasWidth: 640,
        canvasHeight: 480,
        // H5端音频发送缓冲，用于聚合到32KB再发送
        audioSendBuffer: new Uint8Array(0),
        // 小程序端Whisper音频缓冲，用于聚合到32KB再发送
        whisperAudioBuffer: new Uint8Array(0),
        
        // 音量显示相关
        volumeLevel: 0,
        volumePercentage: 0,
        lastVolume: 0, // 用于平滑处理
        lastUpdateTime: 0, // 上次更新时间
        volumeHistory: [], // 音量历史记录，用于校准
        maxVolumeSeen: 0, // 观察到的最大音量
        
        // Whisper音频测试相关
        whisperTestEnabled: false, // Whisper音频测试状态
        
        // 防作弊相关属性
        hasLeftPage: false, // 是否已经离开过页面
        examStarted: false, // 考试是否已开始
        pageLeaveTime: null, // 页面离开时间
        examEnded: false, // 考试是否已结束（正常或异常）
        examAbnormal: false, // 考试是否异常结束
        
        // 考核相关属性
        examTaskId: null,
        currentQuestionIndex: 0,
        questions: [],
        showCheatModal: true,
        // 不再需要agreeTerms状态，按钮默认可用
        showExamContent: false,
        showExamCompleteModal: false,
        remainingTime: MAX_QUESTION_TIME,
        questionTimer: null,
        totalQuestions: 0,
        // 考试时间信息
        scheduledStartTime: null,
        scheduledEndTime: null,
        // 考试状态标志
        examEnded: false,
        examAbnormal: false,
        // 状态检查定时器
        examStatusCheckTimer: null,
        // 考试结果数据
        examResult: null,
        loadingResult: false,
        // 识别结果累计
        recognizedTextLog: '',
        // 当前题目的识别文本（用于多次语音输入）
        currentQuestionText: '',
        
        // Token刷新相关
        refreshTokenTimer: null,
        // 考试状态检查定时器
        examStatusCheckTimer: null
      };
    },
    
    
    computed: {
      currentQuestion() {
        return this.questions[this.currentQuestionIndex] || null;
      }
    },
    
    methods: {
      // 计算音频音量
      calculateVolume(audioData) {
        if (!audioData || audioData.length === 0) {
          console.log('[DEBUG] 音频数据为空或无效');
          return 0;
        }
        
        let sum = 0;
        let maxValue = 0;
        let validSamples = 0;
        let minValue = Infinity;
        let maxValueRaw = -Infinity;
        
        // 处理不同类型的音频数据
        for (let i = 0; i < audioData.length; i++) {
          let sample = audioData[i];
          
          // 记录原始值范围
          minValue = Math.min(minValue, sample);
          maxValueRaw = Math.max(maxValueRaw, sample);
          
          // 如果是Uint8Array（0-255），转换为-1到1的范围
          if (sample > 1) {
            sample = (sample - 128) / 128;
          }
          
          if (Math.abs(sample) > 0.001) {
            validSamples++;
          }
          
          sum += Math.abs(sample);
          maxValue = Math.max(maxValue, Math.abs(sample));
        }
        
        // 使用简单的平均值计算
        const avg = sum / audioData.length;
        
        // 重新设计音量映射逻辑
        let volume = 0;
        if (avg > 0) {
          // 根据数据类型使用不同的映射策略
          if (audioData.constructor.name === 'Float32Array') {
            // Float32Array通常在-1到1之间，使用更保守的映射
            volume = Math.min(100, avg * 100);
          } else if (audioData.constructor.name === 'Uint8Array') {
            // Uint8Array在0-255之间，需要更小的映射系数
            volume = Math.min(100, avg * 50);
          } else {
            // 其他类型，使用默认映射
            volume = Math.min(100, avg * 100);
          }
          
          // 如果平均值太小，使用峰值作为补充
          if (volume < 5 && maxValue > 0.01) {
            volume = Math.min(100, maxValue * 50);
          }
        }
        
        // 添加调试信息（每次计算都输出，便于排查）
        console.log(`[DEBUG] 音量计算: 数据类型=${audioData.constructor.name}, 长度=${audioData.length}, 有效样本=${validSamples}, 原始范围=[${minValue.toFixed(2)}, ${maxValueRaw.toFixed(2)}], 平均值=${avg.toFixed(4)}, 最大值=${maxValue.toFixed(4)}, 音量=${volume.toFixed(2)}%`);
        
        return Math.round(volume);
      },
      
      // 更新音量显示
      updateVolumeDisplay(audioData) {
        const currentVolume = this.calculateVolume(audioData);
        const currentTime = Date.now();
        
        // 改进的平滑处理：更快的响应和更自然的衰减
        let smoothedVolume;
        
        if (this.lastUpdateTime === 0) {
          // 第一次更新
          smoothedVolume = currentVolume;
        } else {
          const timeDiff = currentTime - this.lastUpdateTime;
          
          if (currentVolume > this.lastVolume) {
            // 音量上升：快速响应（80%权重给新值）
            const riseFactor = 0.8;
            smoothedVolume = this.lastVolume * (1 - riseFactor) + currentVolume * riseFactor;
          } else {
            // 音量下降：更快的衰减，确保能快速下降到实际值
            const decayFactor = Math.min(0.6, timeDiff / 30); // 更快的衰减
            smoothedVolume = Math.max(currentVolume, this.lastVolume * (1 - decayFactor));
          }
        }
        
        // 记录音量历史，用于校准
        this.volumeHistory.push(smoothedVolume);
        if (this.volumeHistory.length > 100) {
          this.volumeHistory.shift(); // 保持最近100个记录
        }
        
        // 更新观察到的最大音量
        if (smoothedVolume > this.maxVolumeSeen) {
          this.maxVolumeSeen = smoothedVolume;
        }
        
        this.volumeLevel = Math.round(smoothedVolume);
        this.volumePercentage = Math.round(smoothedVolume);
        this.lastVolume = smoothedVolume;
        this.lastUpdateTime = currentTime;
        
        // 添加调试信息（每50次更新一次，避免日志过多）
        if (Math.random() < 0.02) { // 2%的概率输出调试信息
          const avgVolume = this.volumeHistory.reduce((a, b) => a + b, 0) / this.volumeHistory.length;
          console.log('[DEBUG] 音量更新 - 原始:', currentVolume + '%', '平滑后:', Math.round(smoothedVolume) + '%', '平均:', avgVolume.toFixed(1) + '%', '最大:', this.maxVolumeSeen.toFixed(1) + '%');
        }
      },
      
      // 降采样音频数据，参考Whisper微服务实现
      downsampleBuffer(buffer, inputSampleRate, targetSampleRate) {
        if (inputSampleRate === targetSampleRate) {
          return buffer;
        }
        const sampleRateRatio = inputSampleRate / targetSampleRate;
        const newLength = Math.round(buffer.length / sampleRateRatio);
        const result = new Float32Array(newLength);
        let offsetResult = 0;
        let offsetBuffer = 0;
        while (offsetResult < result.length) {
          const nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
          // 对offsetBuffer到nextOffsetBuffer之间的样本求平均
          let accum = 0, count = 0;
          for (let i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
            accum += buffer[i];
            count++;
          }
          result[offsetResult] = count ? accum / count : 0;
          offsetResult++;
          offsetBuffer = nextOffsetBuffer;
        }
        return result;
      },
      
      // 将Float32Array转换为Int16 PCM格式（Whisper微服务要求）
      floatTo16BitPCM(float32Array) {
        const l = float32Array.length;
        const buf = new ArrayBuffer(l * 2);
        const view = new DataView(buf);
        for (let i = 0; i < l; i++) {
          let s = Math.max(-1, Math.min(1, float32Array[i]));
          s = s < 0 ? s * 0x8000 : s * 0x7fff;
          view.setInt16(i * 2, s, true); // little-endian
        }
        return new Uint8Array(buf);
      },

      // 合并Uint8Array
      concatUint8(a, b) {
        const c = new Uint8Array(a.length + b.length);
        c.set(a, 0);
        c.set(b, a.length);
        return c;
      },

      // 发送音频数据到Whisper微服务（累积到合适大小再发送）
      maybeSendBytes(bytes) {
        // 验证输入数据格式
        if (!bytes || bytes.length === 0) {
          console.warn('[DEBUG] 音频数据为空，跳过发送');
          return;
        }
        
        // 确保输入数据是偶数长度（Int16格式要求）
        if (bytes.length % 2 !== 0) {
          console.warn('[DEBUG] 输入音频数据大小不是Int16格式的整数倍:', bytes.length);
          // 截断到最近的偶数
          const correctedLength = Math.floor(bytes.length / 2) * 2;
          bytes = bytes.slice(0, correctedLength);
          console.log('[DEBUG] 输入数据修正后大小:', bytes.length);
        }
        
        this.whisperAudioBuffer = this.concatUint8(this.whisperAudioBuffer, bytes);
        // PCM格式使用适中的缓冲大小，平衡延迟和稳定性
        // 16KB对于PCM格式已经足够，实现良好的实时性
        const CHUNK_BYTES = 8192; // 8KB块大小，更快出块，提升实时性与保存命中率
        while (this.whisperAudioBuffer.length >= CHUNK_BYTES) {
          const toSend = this.whisperAudioBuffer.slice(0, CHUNK_BYTES);
          this.whisperAudioBuffer = this.whisperAudioBuffer.slice(CHUNK_BYTES);
          
          // 验证发送数据格式
          if (toSend.length !== CHUNK_BYTES) {
            console.warn('[DEBUG] 发送数据大小异常:', toSend.length, '期望:', CHUNK_BYTES);
          }
          
          // 确保数据大小是Int16格式的整数倍（每个样本2字节）
          if (toSend.length % 2 !== 0) {
            console.error('[DEBUG] 音频数据大小不是Int16格式的整数倍:', toSend.length);
            // 如果数据大小不是2的整数倍，截断到最近的偶数
            const adjustedLength = Math.floor(toSend.length / 2) * 2;
            const adjustedData = toSend.slice(0, adjustedLength);
            console.log('[DEBUG] 调整数据大小从', toSend.length, '到', adjustedLength);
            this.sendAudioChunk(adjustedData);
            continue;
          }
          
          // 使用统一的发送方法
          this.sendAudioChunk(toSend);
        }
      },
      
      // 确保音频数据符合Int16格式要求
      ensureInt16Format(audioBytes) {
        console.log('[DEBUG] 开始数据格式修正，原始大小:', audioBytes.length);
        
        // 如果数据大小不是2的整数倍，进行修正
        if (audioBytes.length % 2 !== 0) {
          console.warn('[DEBUG] 数据大小不是2的整数倍，进行修正:', audioBytes.length);
          // 截断到最近的偶数
          const correctedLength = Math.floor(audioBytes.length / 2) * 2;
          const correctedBytes = audioBytes.slice(0, correctedLength);
          console.log('[DEBUG] 修正后大小:', correctedBytes.length);
          return correctedBytes;
        }
        
        // 如果数据大小是2的整数倍，直接返回
        console.log('[DEBUG] 数据格式正确，大小:', audioBytes.length);
        return audioBytes;
      },
      
      // 将Uint8Array转换为真正的Int16 PCM格式
      convertToInt16PCM(audioBytes) {
        console.log('[DEBUG] 开始转换音频数据为Int16 PCM格式，原始大小:', audioBytes.length);
        
        // 确保数据大小是2的整数倍
        if (audioBytes.length % 2 !== 0) {
          console.warn('[DEBUG] 数据大小不是2的整数倍，进行修正');
          const correctedLength = Math.floor(audioBytes.length / 2) * 2;
          audioBytes = audioBytes.slice(0, correctedLength);
        }
        
        // 检查数据是否已经是正确的Int16格式
        // 通过检查数据范围来判断
        let isAlreadyInt16 = true;
        let sampleCount = 0;
        
        for (let i = 0; i < Math.min(100, audioBytes.length); i += 2) {
          if (i + 1 < audioBytes.length) {
            // 将两个字节组合成Int16值（little-endian）
            const int16Value = audioBytes[i] | (audioBytes[i + 1] << 8);
            sampleCount++;
            
            // 检查值是否在合理的Int16范围内
            if (int16Value > 32767 || int16Value < -32768) {
              isAlreadyInt16 = false;
              break;
            }
          }
        }
        
        if (isAlreadyInt16 && sampleCount > 0) {
          console.log('[DEBUG] 数据已经是Int16格式，直接返回');
          return audioBytes;
        }
        
        console.log('[DEBUG] 数据不是Int16格式，进行转换');
        
        // 将Uint8Array转换为Float32Array，然后转换为Int16 PCM
        const floatArray = new Float32Array(audioBytes.length);
        for (let i = 0; i < audioBytes.length; i++) {
          // 将0-255范围转换为-1到1范围
          floatArray[i] = (audioBytes[i] - 128) / 128;
        }
        
        // 使用现有的floatTo16BitPCM方法转换
        const int16PCM = this.floatTo16BitPCM(floatArray);
        console.log('[DEBUG] 转换完成，输出大小:', int16PCM.length);
        
        return int16PCM;
      },
      
      // 统一的音频数据发送方法
      sendAudioChunk(audioData) {
        // 最终验证：确保数据大小是Int16格式的整数倍
        if (audioData.length % 2 !== 0) {
          console.error('[DEBUG] 发送前最终检查：数据大小不是Int16格式的整数倍:', audioData.length);
          // 强制修正数据
          const correctedLength = Math.floor(audioData.length / 2) * 2;
          audioData = audioData.slice(0, correctedLength);
          console.log('[DEBUG] 强制修正数据大小到:', audioData.length);
        }
        
        // 额外验证：确保数据不为空
        if (audioData.length === 0) {
          console.warn('[DEBUG] 音频数据为空，跳过发送');
          return;
        }
        
        // 验证修正后的数据大小
        if (audioData.length % 2 !== 0) {
          console.error('[DEBUG] 修正后数据大小仍不是Int16格式的整数倍:', audioData.length);
          return;
        }
        
        //console.log('[DEBUG] 准备发送音频数据，大小:', audioData.length, '字节');
        //console.log('[DEBUG] 数据前10字节:', Array.from(audioData.slice(0, 10)));
        //console.log('[DEBUG] 数据后10字节:', Array.from(audioData.slice(-10)));
        
        // #ifdef H5
        // H5环境：发送到后端桥接服务，带0x02前缀
        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
          try {
            const messageType = new Uint8Array([0x02]);
            const combinedData = new Uint8Array(messageType.length + audioData.length);
            combinedData.set(messageType);
            combinedData.set(audioData, messageType.length);
            this.ws.send(combinedData.buffer);
            //console.log('[DEBUG] 发送', audioData.length, '字节音频数据到后端 (带0x02前缀)');
          } catch (e) {
            console.error('[DEBUG] 发送音频数据失败:', e);
          }
        }
        // #endif
        
        // #ifndef H5
        // 小程序环境使用uni-app的WebSocket API
        if (this.ws) {
          try {
            const messageType = new Uint8Array([0x02]);
            const combinedData = new Uint8Array(messageType.length + audioData.length);
            combinedData.set(messageType);
            combinedData.set(audioData, messageType.length);
            this.ws.send({
              data: combinedData.buffer,
              success: () => {
                console.log('[DEBUG] 发送', audioData.length, '字节音频数据到后端成功 (带0x02前缀)');
              },
              fail: (error) => {
                console.error('[DEBUG] 发送音频数据失败:', error);
              }
            });
          } catch (e) {
            console.error('[DEBUG] 发送音频数据异常:', e);
          }
        }
        // #endif
      },
        
        // 重置音量显示
      resetVolumeDisplay() {
        this.volumeLevel = 0;
        this.volumePercentage = 0;
        this.lastVolume = 0;
        this.lastUpdateTime = 0;
        this.volumeHistory = [];
        this.maxVolumeSeen = 0;
        console.log('[DEBUG] 音量显示已重置');
      },
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      // 实时播放发送给Whisper的音频（仅在测试模式下）
      playBackendAudio(audioData) {
        // 现在使用直接连接的方式，这个方法主要用于小程序环境
        if (!this.whisperTestEnabled) {
          return; // 只在Whisper测试启用时播放
        }
          
          // #ifndef H5
        // 小程序环境暂不支持实时播放
          console.log('[DEBUG] 小程序环境暂不支持实时音频播放');
          // #endif
      },

      // Whisper音频测试
      testWhisperAudio() {
        this.whisperTestEnabled = !this.whisperTestEnabled;
        
          // #ifdef H5
        // H5环境下动态控制音频连接
        if (this.processor && this.audioContext) {
          if (this.whisperTestEnabled) {
            // 连接到扬声器
            this.processor.connect(this.audioContext.destination);
            this.showToast('Whisper音频测试已开启 - 可以听到麦克风实时声音');
            console.log('[DEBUG] Whisper音频测试已开启，音频已连接到扬声器');
          } else {
            // 断开扬声器连接
            try {
              this.processor.disconnect(this.audioContext.destination);
              this.showToast('Whisper音频测试已关闭');
              console.log('[DEBUG] Whisper音频测试已关闭，音频已断开扬声器');
            } catch (e) {
              // 忽略断开连接时的错误
              console.log('[DEBUG] 断开音频连接时出现错误（可忽略）:', e);
            }
          }
        } else {
          this.showToast('音频系统未就绪，请先开始考试');
          console.log('[DEBUG] 音频系统状态检查:', {
            processor: !!this.processor,
            audioContext: !!this.audioContext,
            audioStream: !!this.audioStream,
            audioStatus: this.audioStatus
          });
        }
          // #endif
        
        // #ifndef H5
        // 小程序环境下显示提示
        if (this.whisperTestEnabled) {
          this.showToast('Whisper音频测试已开启 - 小程序环境暂不支持实时播放');
          console.log('[DEBUG] Whisper音频测试已开启（小程序环境）');
        } else {
          this.showToast('Whisper音频测试已关闭');
          console.log('[DEBUG] Whisper音频测试已关闭（小程序环境）');
        }
        // #endif
      },
      
      // 测试音频格式
      testAudioFormat() {
        console.log('[DEBUG] 开始测试音频格式...');
        
        // 创建测试数据
        const testSizes = [1024, 1025, 16384, 16385, 8192, 8193];
        
        testSizes.forEach(size => {
          console.log(`[DEBUG] 测试数据大小: ${size} 字节`);
          console.log(`[DEBUG] 是否为2的整数倍: ${size % 2 === 0}`);
          
          // 创建测试数据
          const testData = new Uint8Array(size);
          for (let i = 0; i < size; i++) {
            testData[i] = Math.floor(Math.random() * 256);
          }
          
          // 测试格式修正
          const correctedData = this.ensureInt16Format(testData);
          console.log(`[DEBUG] 修正后大小: ${correctedData.length} 字节`);
          console.log(`[DEBUG] 修正后是否为2的整数倍: ${correctedData.length % 2 === 0}`);
          
          // 如果WebSocket连接正常，发送测试数据
          if (this.ws && this.ws.readyState === WebSocket.OPEN) {
            console.log(`[DEBUG] 发送测试数据: ${correctedData.length} 字节`);
            this.sendAudioChunk(correctedData);
          } else {
            console.log('[DEBUG] WebSocket未连接，跳过发送测试');
          }
        });
        
        this.showToast('音频格式测试完成，请查看控制台日志');
      },
      
      // 测试Int16转换
      testInt16Conversion() {
        console.log('[DEBUG] 开始测试Int16转换...');
        
        // 创建模拟的Uint8Array数据（模拟小程序录音数据）
        const testData = new Uint8Array(1024);
        for (let i = 0; i < testData.length; i++) {
          testData[i] = Math.floor(Math.random() * 256);
        }
        
        console.log('[DEBUG] 原始测试数据大小:', testData.length);
        console.log('[DEBUG] 原始数据前10字节:', Array.from(testData.slice(0, 10)));
        
        // 测试转换
        const convertedData = this.convertToInt16PCM(testData);
        console.log('[DEBUG] 转换后数据大小:', convertedData.length);
        console.log('[DEBUG] 转换后数据前10字节:', Array.from(convertedData.slice(0, 10)));
        
        // 验证转换后的数据是否为有效的Int16格式
        let validInt16Count = 0;
        for (let i = 0; i < Math.min(20, convertedData.length); i += 2) {
          if (i + 1 < convertedData.length) {
            const int16Value = convertedData[i] | (convertedData[i + 1] << 8);
            if (int16Value >= -32768 && int16Value <= 32767) {
              validInt16Count++;
            }
          }
        }
        
        console.log('[DEBUG] 有效Int16样本数:', validInt16Count);
        console.log('[DEBUG] 转换成功率:', (validInt16Count / 10 * 100).toFixed(1) + '%');
        
        // 如果WebSocket连接正常，发送测试数据
        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
          console.log('[DEBUG] 发送转换后的测试数据');
          this.sendAudioChunk(convertedData);
        } else {
          console.log('[DEBUG] WebSocket未连接，跳过发送测试');
        }
        
        this.showToast('Int16转换测试完成，请查看控制台日志');
      },
      
      // 由于不再需要勾选同意，此方法已被移除
      // handleAgreeChange方法已删除，按钮现在默认可用
      
      // 初始化页面，获取考核任务详情
      async initPage() {
        try {
          // 重置音视频状态，避免显示之前的状态
          this.videoStatus = '未启动';
          this.audioStatus = '未启动';
          console.log('[DEBUG] 页面初始化，重置音视频状态为未启动');
          
          // 检查是否为重新进入（防作弊检测）
          await this.checkAntiCheat();
          
          // 从URL获取参数 - uni-app方式
          const pages = getCurrentPages();
          const currentPage = pages[pages.length - 1];
          const options = currentPage.options;
          const examTaskId = options.examTaskId;
          
          console.log('获取到的examTaskId:', examTaskId);
          
          if (!examTaskId) {
            this.showToast('缺少考核任务ID');
            return;
          }
          
          this.examTaskId = examTaskId;
          
          // 获取考核任务详情
          await this.fetchExamTaskDetail();
        } catch (error) {
          console.error('初始化页面失败:', error);
          this.showToast('页面初始化失败');
        }
      },
      
      // 防作弊检测
      async checkAntiCheat() {
        try {
          const userInfo = uni.getStorageSync('userInfo');
          if (!userInfo || !userInfo.id) {
            console.log('[ANTI-CHEAT] 没有用户信息，跳过防作弊检测');
            return;
          }
          
          // 首先检查后端考试状态
          if (this.examTaskId) {
            try {
              const response = await uni.request({
                url: `${NETWORK_CONFIG.API_BASE_URL}/exam/task/detail?examTaskId=${this.examTaskId}`,
                method: 'GET',
                header: {
                  'Access-Token': `Bearer ${uni.getStorageSync('accessToken')}`
                }
              });
              
              if (response.statusCode === 200 && response.data) {
                const examTask = response.data.examTask || response.data;
                if (examTask.status === 4) {
                  console.log('[ANTI-CHEAT] 后端检测到考试已异常结束，直接显示异常状态');
                  this.examAbnormal = true;
                  this.examEnded = true;
                  this.showExamCompleteModal = true;
                  this.showExamContent = false;
                  this.showToast('考试已异常结束');
                  return;
                } else if (examTask.status === 3) {
                  console.log('[ANTI-CHEAT] 后端检测到考试已完成，直接显示完成状态');
                  this.examEnded = true;
                  await this.showExamCompleteModalWithResult();
                  this.showToast('考试已完成');
                  return;
                }
              }
            } catch (error) {
              console.error('[ANTI-CHEAT] 检查后端状态失败:', error);
            }
          }
          
          const examSessionKey = `exam_session_${userInfo.id}`;
          const examSessionData = uni.getStorageSync(examSessionKey);
          
          console.log('[ANTI-CHEAT] 检查考试会话数据:', examSessionData);
          
          if (examSessionData) {
            // 检查是否已经离开过页面
            if (examSessionData.hasLeftPage && examSessionData.examStarted) {
              console.log('[ANTI-CHEAT] 检测到用户已离开页面，触发防作弊机制');
              
              // 直接结束考试并清空答案
              await this.terminateExamForCheating();
              return;
            }
          }
          
          // 初始化或更新考试会话数据
          const newSessionData = {
            examStarted: false,
            hasLeftPage: false,
            pageEnterTime: Date.now(),
            pageLeaveTime: null
          };
          uni.setStorageSync(examSessionKey, newSessionData);
          console.log('[ANTI-CHEAT] 初始化考试会话数据');
          
        } catch (error) {
          console.error('[ANTI-CHEAT] 防作弊检测失败:', error);
        }
      },
      
      // 终止考试（防作弊）
      async terminateExamForCheating() {
        try {
          console.log('[ANTI-CHEAT] 开始终止考试，清空所有答案');
          
          // 显示防作弊提示
          uni.showModal({
            title: '防作弊检测',
            content: '检测到您已离开考试页面，考试已自动结束，所有答案已清空。',
            showCancel: false,
            confirmText: '确定',
            success: () => {
              // 清空所有答案并结束考试
              this.clearAllAnswersAndEndExam();
            }
          });
          
        } catch (error) {
          console.error('[ANTI-CHEAT] 终止考试失败:', error);
        }
      },
      
      // 清空所有答案并结束考试
      async clearAllAnswersAndEndExam() {
        try {
          const userInfo = uni.getStorageSync('userInfo');
          if (!userInfo || !userInfo.id) {
            return;
          }
          
          // 调用后端API清空所有答案
          const response = await uni.request({
            url: `${NETWORK_CONFIG.API_BASE_URL}/exam/clear-answers?userId=${userInfo.id}`,
            method: 'POST',
            header: {
              'Access-Token': `Bearer ${uni.getStorageSync('accessToken')}`
            }
          });
          
          if (response.statusCode === 200) {
            console.log('[ANTI-CHEAT] 所有答案已清空');
          }
          
          // 清理本地存储
          const examSessionKey = `exam_session_${userInfo.id}`;
          uni.removeStorageSync(examSessionKey);
          
          // 设置考试异常结束状态，停止重连
          this.examAbnormal = true;
          this.examEnded = true;
          
          // 显示考试完成模态框
          this.showExamCompleteModal = true;
          this.showExamContent = false;
          
          // 停止所有媒体流
          this.stopAllMediaAndStreaming();
          
        } catch (error) {
          console.error('[ANTI-CHEAT] 清空答案失败:', error);
        }
      },
      
      // 获取考核任务详情
      async fetchExamTaskDetail() {
        try {
          console.log('准备获取考核任务详情，examTaskId:', this.examTaskId);
          const url = `${NETWORK_CONFIG.API_BASE_URL}/exam/task/detail?examTaskId=${this.examTaskId}`;
          console.log('请求URL:', url);
          
          const response = await uni.request({
            url: url,
            method: 'GET',
            header: {
              'Access-Token': `Bearer ${uni.getStorageSync('accessToken')}`
            }
          });
          
          console.log('响应状态码:', response.statusCode);
          console.log('响应数据:', response.data);
          
          if (response.statusCode === 200 && response.data) {
            const examTask = response.data.examTask || response.data;
            this.questions = examTask.questions || [];
            this.totalQuestions = this.questions.length;
            
            // 保存预约时间信息
            if (examTask.scheduledStartTime) {
              this.scheduledStartTime = examTask.scheduledStartTime;
              console.log('[DEBUG] 预约开始时间:', this.scheduledStartTime);
            }
            if (examTask.scheduledEndTime) {
              this.scheduledEndTime = examTask.scheduledEndTime;
              console.log('[DEBUG] 预约结束时间:', this.scheduledEndTime);
            }
            
            // 检查考试任务状态
            if (examTask.status) {
              console.log('[DEBUG] 考试任务状态:', examTask.status);
              if (examTask.status === 4) {
                // 考试任务已异常结束
                console.log('[DEBUG] 检测到考试任务已异常结束，设置前端状态');
                this.examAbnormal = true;
                this.examEnded = true;
                this.showExamCompleteModal = true;
                this.showExamContent = false;
                this.showToast('考试已异常结束');
                return;
              } else if (examTask.status === 3) {
                // 考试任务已完成
                console.log('[DEBUG] 检测到考试任务已完成，设置前端状态');
                this.examEnded = true;
                this.showExamCompleteModal = true;
                this.showExamContent = false;
                this.showToast('考试已完成');
                return;
              }
            }
          } else {
            throw new Error(response.data || '获取考核任务详情失败');
          }
        } catch (error) {
          console.error('获取考核任务详情失败:', error);
          this.showToast('获取考核任务详情失败: ' + error.message);
        }
      },
      
      // 标记考试已开始（防作弊）
      markExamStarted() {
        try {
          const userInfo = uni.getStorageSync('userInfo');
          if (!userInfo || !userInfo.id) {
            return;
          }
          
          const examSessionKey = `exam_session_${userInfo.id}`;
          const examSessionData = uni.getStorageSync(examSessionKey) || {};
          
          examSessionData.examStarted = true;
          examSessionData.examStartTime = Date.now();
          
          uni.setStorageSync(examSessionKey, examSessionData);
          console.log('[ANTI-CHEAT] 考试已标记为开始');
          
        } catch (error) {
          console.error('[ANTI-CHEAT] 标记考试开始失败:', error);
        }
      },
      
      // 验证考试时间
      validateExamTime() {
        if (!this.scheduledStartTime || !this.scheduledEndTime) {
          console.log('[DEBUG] 没有预约时间信息，跳过时间验证');
          return { valid: true };
        }
        
        const now = new Date();
        
        // 解析预约时间字符串，避免时区转换问题
        const parseLocalDateTime = (dateTimeStr) => {
          // 格式: "2025-09-21T19:35"
          const [datePart, timePart] = dateTimeStr.split('T');
          const [year, month, day] = datePart.split('-').map(Number);
          const [hour, minute] = timePart.split(':').map(Number);
          
          // 创建本地时间，不进行时区转换
          return new Date(year, month - 1, day, hour, minute);
        };
        
        const startTime = parseLocalDateTime(this.scheduledStartTime);
        const endTime = parseLocalDateTime(this.scheduledEndTime);
        
        console.log('[DEBUG] 时间验证 - 当前时间:', now.toLocaleString());
        console.log('[DEBUG] 时间验证 - 预约开始时间:', startTime.toLocaleString());
        console.log('[DEBUG] 时间验证 - 预约结束时间:', endTime.toLocaleString());
        console.log('[DEBUG] 时间验证 - 原始预约时间字符串:', this.scheduledStartTime);
        
        if (now < startTime) {
          const timeDiff = Math.ceil((startTime - now) / (1000 * 60)); // 分钟
          return {
            valid: false,
            message: `预约时间还未到！请在 ${startTime.toLocaleString()} 再开始考核（还需等待 ${timeDiff} 分钟）`
          };
        }
        
        if (now > endTime) {
          return {
            valid: false,
            message: `考试时间已过！预约结束时间为 ${endTime.toLocaleString()}`
          };
        }
        
        return { valid: true };
      },
      
      // 开始考试
      async startExam() {
        try {
          // 验证考试时间
          const timeValidation = this.validateExamTime();
          if (!timeValidation.valid) {
            this.showToast(timeValidation.message);
            return;
          }
          
          // 标记考试已开始（防作弊）
          this.markExamStarted();
          
          // 调用后端API开始考试
          const response = await uni.request({
            url: `${NETWORK_CONFIG.API_BASE_URL}/exam/start?examTaskId=${this.examTaskId}`,
            method: 'POST',
            header: {
              'Access-Token': `Bearer ${uni.getStorageSync('accessToken')}`
            }
          });
          
          if (response.statusCode === 200) {
            const data = response.data;
            console.log('[DEBUG] 开始考试响应:', data);
            
            // 检查是否有下一个题目
            if (data.nextQuestion) {
              // 有未回答的题目，开始考试
              this.questions = [data.nextQuestion]; // 设置题目数组
              this.currentQuestionIndex = 0; // 重置索引
              
              // 隐藏防作弊提示，显示考试内容
              this.showCheatModal = false;
              this.showExamContent = true;
              
              // 启动摄像头和音频采集
              await this.startCamera();
              
              // 启动当前问题的计时器
              this.startQuestionTimer();
              
              // 启动考试状态检查定时器
              this.startExamStatusCheck();
              
              // 播报当前问题
              this.speakQuestion();
              
              this.showToast('考试已开始，请回答当前题目');
            } else {
              // 所有题目都已完成
              this.showToast('所有题目已完成，考试结束');
              this.navigateBack();
            }
          } else {
            throw new Error(response.data || '开始考试失败');
          }
        } catch (error) {
          console.error('开始考试失败:', error);
          this.showToast(error.message || '开始考试失败');
        }
      },
      
      // 启动当前问题的计时器
      startQuestionTimer() {
        this.remainingTime = MAX_QUESTION_TIME;
        
        if (this.questionTimer) {
          clearInterval(this.questionTimer);
        }
        
        this.questionTimer = setInterval(() => {
          this.remainingTime--;
          
          // 时间到，自动提交答案并进入下一题
          if (this.remainingTime <= 0) {
            this.answerComplete();
          }
        }, 60000); // 每分钟更新一次
      },
      
      // 播报问题
      speakQuestion() {
        if (this.currentQuestion) {
          // #ifdef H5
          // Web环境下使用原生API
          if (window && 'speechSynthesis' in window) {
            try {
              const utterance = new window.SpeechSynthesisUtterance(this.currentQuestion.questionText);
          utterance.lang = 'zh-CN';
              window.speechSynthesis.speak(utterance);
            } catch (error) {
              console.error('语音合成失败:', error);
              // 回退方案：显示问题文本
              this.showToast('问题：' + this.currentQuestion.questionText);
            }
          } else {
            // 不支持语音合成时，显示问题文本
            this.showToast('问题：' + this.currentQuestion.questionText);
          }
          // #endif
          
          // #ifndef H5
          // 非Web环境（如小程序）使用uni-app的语音API或模拟方案
          this.showToast('问题：' + this.currentQuestion.questionText);
          // 实际应用中可考虑使用uni.createInnerAudioContext()播放预录音
          // #endif
        }
      },
      
      // 回答完毕，进入下一题（答案已通过 WebSocket 持久化到后端）
      async answerComplete() {
        console.log('[DEBUG] ========== answerComplete 方法开始执行 ==========');
        console.log('[DEBUG] 当前题目ID:', this.currentQuestion?.id);
        console.log('[DEBUG] 当前题目索引:', this.currentQuestionIndex);
        
        // 清除当前问题的计时器
        if (this.questionTimer) {
          clearInterval(this.questionTimer);
          this.questionTimer = null;
          console.log('[DEBUG] 已清除题目计时器');
        }

        // 先发送题目切换消息，让后端处理文本提交
        console.log('[DEBUG] 开始处理当前题目，题目ID:', this.currentQuestion?.id);
        console.log('[DEBUG] WebSocket状态检查 - ws:', !!this.ws, 'isConnected:', this.isConnected);
        
        // 发送题目切换消息，让后端先处理音频保存和文本提交
        if (this.ws && this.isConnected) {
          try {
            const switchMsg = JSON.stringify({
              type: 'question_switch',
              previousQuestionId: this.currentQuestion.id,
              newQuestionId: this.currentQuestion.id // 当前题目ID
            });
            console.log('[DEBUG] 准备发送题目切换消息:', switchMsg);
            this.ws.send(switchMsg);
            console.log('[DEBUG] 已发送题目切换消息（处理当前题目）:', switchMsg);
            
            // 等待一下确保消息发送完成
            console.log('[DEBUG] 等待500ms确保消息发送完成...');
            await new Promise(resolve => setTimeout(resolve, 500));
            console.log('[DEBUG] 等待完成，继续执行后续逻辑');
          } catch (e) {
            console.warn('[DEBUG] 发送题目切换消息失败:', e);
          }
        } else {
          console.warn('[DEBUG] WebSocket未连接，无法发送题目切换消息');
        }
        
        // 然后提交当前题目的答案（主要是为了触发考试完成检查）
        console.log('[DEBUG] 开始提交当前题目答案，题目ID:', this.currentQuestion?.id);
        await this.submitCurrentQuestionAnswer();

        console.log('[DEBUG] 当前题目答案提交完成');

        // 获取下一个未回答的题目
        try {
          console.log('[DEBUG] 开始获取下一个题目，examTaskId:', this.examTaskId);
          const response = await uni.request({
            url: `${NETWORK_CONFIG.API_BASE_URL}/exam/next-question?examTaskId=${this.examTaskId}`,
            method: 'GET',
            header: {
              'Access-Token': `Bearer ${uni.getStorageSync('accessToken')}`
            }
          });

          if (response.statusCode === 200) {
            const data = response.data;
            console.log('[DEBUG] 获取下一个题目响应:', data);
            console.log('[DEBUG] nextQuestion:', data.nextQuestion);
            console.log('[DEBUG] isCompleted:', data.isCompleted);

            if (data.nextQuestion && !data.isCompleted) {
              // 有下一个未回答的题目
              const previousQuestionId = this.currentQuestion.id; // 保存当前题目ID作为上一题
              console.log('[DEBUG] 准备切换题目，previousQuestionId:', previousQuestionId, 'newQuestionId:', data.nextQuestion.id);
              this.questions.push(data.nextQuestion); // 添加新题目到数组
              this.currentQuestionIndex++; // 增加题目索引
              console.log('[DEBUG] 切换到下一题，题目ID:', this.currentQuestion.id);

              // 清空当前题目的文本，为下一题做准备
              this.clearRecognition();

              // 重新连接WebSocket以携带新的questionId（用于后端分题落库）
              try {
                if (this.ws) {
                  console.log('[DEBUG] WebSocket连接存在，准备发送题目切换消息');
                  // 发送题目切换消息，让后端先处理音频保存
                  const switchMsg = JSON.stringify({
                    type: 'question_switch',
                    previousQuestionId: previousQuestionId,
                    newQuestionId: data.nextQuestion.id
                  });
                  this.ws.send(switchMsg);
                  console.log('[DEBUG] 已发送题目切换消息:', switchMsg);
                  
                  // 等待一下确保消息发送完成
                  await new Promise(resolve => setTimeout(resolve, 500));
                  
                  // 然后关闭连接
                  try { this.ws.close && this.ws.close(); } catch (e) {}
                  this.ws = null;
                  this.isConnected = false;
                }
                // 重连
                this.connectWebSocket();
              } catch (e) {
                console.warn('[DEBUG] 切换题目时重连WebSocket失败:', e);
              }

              // 启动下一题计时与播报
              this.startQuestionTimer();
              this.speakQuestion();
            } else {
              // 所有题目都已完成，结束考试
              console.log('[DEBUG] 所有题目已完成，结束考试');
              console.log('[DEBUG] nextQuestion:', data.nextQuestion);
              console.log('[DEBUG] isCompleted:', data.isCompleted);
              
              // 设置考试正常结束状态，停止重连
              this.examEnded = true;
              
              await this.showExamCompleteModalWithResult();
              this.stopAllMediaAndStreaming();
            }
          } else {
            throw new Error(response.data || '获取下一个题目失败');
          }
        } catch (error) {
          console.error('[DEBUG] 获取下一个题目失败:', error);
          this.showToast('获取下一个题目失败: ' + error.message);
          // 即使失败也结束考试，避免卡住
          
          // 设置考试异常结束状态，停止重连
          this.examAbnormal = true;
          this.examEnded = true;
          
          this.showExamCompleteModal = true;
          this.stopAllMediaAndStreaming();
        }
      },
      
      // 提交当前题目的答案 - 文本答案由后端自动保存，防止前端篡改
      async submitCurrentQuestionAnswer() {
        try {
          if (!this.currentQuestion || !this.currentQuestion.id) {
            console.warn('[DEBUG] 当前题目不存在，无法提交答案');
            return;
          }

          console.log('[DEBUG] 准备提交当前题目答案:', {
            questionId: this.currentQuestion.id,
            examTaskId: this.examTaskId,
            note: '文本答案由后端自动保存，防止前端篡改'
          });

          // 使用REST API提交答案（不包含文本，文本由后端自动保存）
          const response = await uni.request({
            url: `${NETWORK_CONFIG.API_BASE_URL}/exam/answer/submit`,
            method: 'POST',
            header: {
              'Content-Type': 'application/json',
              'Access-Token': `Bearer ${uni.getStorageSync('accessToken')}`
            },
            data: {
              questionId: this.currentQuestion.id,
              examTaskId: this.examTaskId,
              answerText: null, // 不提交文本，由后端自动保存识别结果
              answerAudio: null // 音频路径由后端在题目切换时处理
            }
          });

          if (response.statusCode === 200) {
            console.log('[DEBUG] 题目答案提交成功:', response.data);
            this.showToast('答案已提交');
          } else {
            console.error('[DEBUG] 题目答案提交失败:', response.data);
            this.showToast('答案提交失败，请重试');
            throw new Error(response.data || '提交失败');
          }

          // 注意：文本清空在answerComplete()方法中统一处理，避免重复清空
          
        } catch (error) {
          console.error('[DEBUG] 提交当前题目答案失败:', error);
          this.showToast('答案提交失败: ' + error.message);
          
          // 提交失败时的重试逻辑
          try {
            console.log('[DEBUG] 尝试重试提交答案...');
            await new Promise(resolve => setTimeout(resolve, 1000));
            
            const retryResponse = await uni.request({
              url: `${NETWORK_CONFIG.API_BASE_URL}/exam/answer/submit`,
              method: 'POST',
              header: {
                'Content-Type': 'application/json',
                'Access-Token': `Bearer ${uni.getStorageSync('accessToken')}`
              },
              data: {
                questionId: this.currentQuestion.id,
                examTaskId: this.examTaskId,
                answerText: null, // 不提交文本，由后端自动保存识别结果
                answerAudio: null
              }
            });
            
            if (retryResponse.statusCode === 200) {
              console.log('[DEBUG] 重试提交成功');
              this.showToast('答案重试提交成功');
            } else {
              throw new Error('重试也失败');
            }
          } catch (retryError) {
            console.error('[DEBUG] 重试提交也失败:', retryError);
            this.showToast('答案提交失败，请检查网络连接');
          }
        }
      },

      // 返回上一页
      navigateBack() {
        uni.navigateBack();
      },
      
      // 获取考试结果
      async fetchExamResult() {
        if (!this.examTaskId) {
          console.warn('[DEBUG] 没有examTaskId，无法获取考试结果');
          return;
        }
        
        try {
          this.loadingResult = true;
          console.log('[DEBUG] 开始获取考试结果，examTaskId:', this.examTaskId);
          
          const response = await uni.request({
            url: `${NETWORK_CONFIG.API_BASE_URL}/exam/result?examTaskId=${this.examTaskId}`,
            method: 'GET',
            header: {
              'Access-Token': `Bearer ${uni.getStorageSync('accessToken')}`
            }
          });
          
          if (response.statusCode === 200 && response.data && response.data.success) {
            this.examResult = response.data.result;
            console.log('[DEBUG] 考试结果获取成功:', this.examResult);
          } else {
            console.error('[DEBUG] 获取考试结果失败:', response.data);
            this.showToast('获取考试结果失败');
          }
        } catch (error) {
          console.error('[DEBUG] 获取考试结果异常:', error);
          this.showToast('获取考试结果失败: ' + error.message);
        } finally {
          this.loadingResult = false;
        }
      },
      
      // 跳转到任务详情页
      goToTaskDetail(taskId) {
        if (!taskId) {
          this.showToast('任务ID不存在');
          return;
        }
        
        console.log('[DEBUG] 跳转到任务详情页，taskId:', taskId);
        uni.navigateTo({
          url: `/pages/task/detail/index?taskId=${taskId}`
        });
      },
      
      // 显示考试完成模态框并获取结果
      async showExamCompleteModalWithResult() {
        this.showExamCompleteModal = true;
        this.showExamContent = false;
        
        // 如果不是异常状态，尝试获取考试结果
        if (!this.examAbnormal && this.examTaskId) {
          await this.fetchExamResult();
        }
      },

      // 识别结果相关
      appendRecognition(text) {
        const line = String(text).trim();
        if (!line) return;
        
        // 累积到当前题目文本（支持多次语音输入）
        if (this.currentQuestionText) {
          // 检查是否需要添加分隔符（如果新文本与现有文本不连贯）
          const lastChar = this.currentQuestionText.slice(-1);
          const firstChar = line.charAt(0);
          
          // 如果现有文本以标点符号结尾，或者新文本以标点符号开头，直接连接
          if (lastChar.match(/[。！？，、；：]/) || firstChar.match(/[。！？，、；：]/)) {
            this.currentQuestionText += line;
          } else {
            // 否则添加空格分隔
            this.currentQuestionText += " " + line;
          }
        } else {
          this.currentQuestionText = line;
        }
        
        // 同时更新显示用的日志（用于UI显示）
        if (this.recognizedTextLog) {
          this.recognizedTextLog += "\n" + line;
        } else {
          this.recognizedTextLog = line;
        }
        
        console.log('[DEBUG] 识别文本已累积，当前题目文本长度:', this.currentQuestionText.length, '最新文本:', line);
      },
      clearRecognition() {
        this.recognizedTextLog = '';
        this.currentQuestionText = '';
        console.log('[DEBUG] 识别文本已清空');
      },
      
      // 清理和格式化文本
      cleanAndFormatText(text) {
        if (!text) return '';
        
        // 移除多余的空白字符
        let cleaned = text.replace(/\s+/g, ' ').trim();
        
        // 移除重复的标点符号
        cleaned = cleaned.replace(/([。！？，、；：])\1+/g, '$1');
        
        // 确保句子结尾有适当的标点符号
        if (cleaned && !cleaned.match(/[。！？]$/)) {
          cleaned += '。';
        }
        
        return cleaned;
      },
      
      // 摄像头错误处理
      onCameraError(e) {
        console.error('摄像头错误:', e);
        uni.showToast({ title: '摄像头错误', icon: 'none' });
        this.videoStatus = '失败';
      },
      
      async startCamera() {
        // 检查用户是否允许摄像头访问
        const cameraPermission = await uni.getSetting({
          scope: 'scope.camera'
        });
        if (!cameraPermission.authSetting['scope.camera']) {
          uni.showToast({ title: '请先授权摄像头访问', icon: 'none' });
          return;
        }


        try {
          // Stop any existing media and streaming before starting new ones
          this.stopAllMediaAndStreaming();
          console.log('开始摄像头捕获和流媒体初始化...');
  
          // #ifdef H5
          // Web环境下使用原生API
          if (window && window.navigator && window.navigator.mediaDevices) {
          // Get video stream
            const videoOnlyStream = await window.navigator.mediaDevices.getUserMedia({
            video: { facingMode: this.videoFacingMode, width: 640, height: 480 }, // Add width/height for capture
            audio: false
          });
          this.videoStream = videoOnlyStream;
          console.log('视频流已获取.');
  
          // Get audio stream separately
            const audioOnlyStream = await window.navigator.mediaDevices.getUserMedia({
            audio: {
              sampleRate: AUDIO_SAMPLE_RATE,
              echoCancellation: true,
              noiseSuppression: true
            }
          });
          this.audioStream = audioOnlyStream;
          console.log('音频流已获取.');
  
          const container = document.getElementById('video-container');
  
          // Create native video element
          const video = document.createElement('video');
          video.autoplay = true;
          video.muted = true;
          video.playsInline = true;
          video.classList.add('video-preview'); // Add the class for consistent styling
          video.style.width = '100%';
          video.style.maxWidth = '640px'; // Align with requested stream and canvas size
          video.style.height = '480px';   // Align with requested stream and canvas size
          video.style.background = 'black';
          video.style.objectFit = 'cover';
          video.style.borderRadius = '10px';
          video.style.marginBottom = '16px';
  
          // Set media stream
          video.srcObject = this.videoStream; // Assign video stream
  
          // Add to container
          container.appendChild(video);
          this.videoElement = video; // Store the video element
          console.log('视频元素已添加到DOM. 尝试播放并等待就绪...');
  
            // Combined play and readiness check
          try {
              await new Promise((resolve, reject) => {
                  const timeoutId = setTimeout(() => {
                      reject(new Error('Video playback or readiness timed out (15s).'));
                  }, 15000);
  
                  const checkAndPlay = () => {
                      // Try to play only if not already playing, paused, or ended
                      if (video.paused || video.ended || video.readyState === 0) {
                          video.play().then(() => {
                              console.log('Video play() initiated successfully.');
                              requestAnimationFrame(checkReadiness); // Continue checking readiness
                          }).catch(playError => {
                              console.error('Video play() failed during checkAndPlay:', playError);
                              clearTimeout(timeoutId);
                              reject(playError); // Reject the promise if play fails
                          });
                      } else {
                          // If video is already playing or ready state is good, just proceed to check readiness
                          requestAnimationFrame(checkReadiness);
                      }
                  };
  
                  const checkReadiness = () => {
                      // Check for HAVE_ENOUGH_DATA (readyState 4) AND valid dimensions
                      if (video.readyState === 4 && video.videoWidth > 0 && video.videoHeight > 0) {
                          console.log('视频已完全就绪（readyState 4, 尺寸稳定）:', video.videoWidth, 'x', video.videoHeight);
                          clearTimeout(timeoutId);
                          resolve(); // Resolve the promise once video is fully ready
                      } else {
                          console.log(`等待视频完全就绪... 当前 readyState: ${video.readyState}, 尺寸: ${video.videoWidth}x${video.videoHeight}`);
                          requestAnimationFrame(checkReadiness); // Continue polling
                      }
                  };
  
                  // Use onloadedmetadata as a trigger for the initial play attempt if not already ready
                  video.onloadedmetadata = () => {
                      console.log('Video metadata loaded. Starting play/readiness check loop.');
                      checkAndPlay();
                  };
  
                  // If video already has metadata or more (e.g., fast loading or re-use), try to play immediately
                  if (video.readyState >= 1) { // HAVE_METADATA or more
                      console.log('Video already has metadata. Attempting immediate play/readiness check.');
                      checkAndPlay();
                  } else {
                      console.log('Video has no metadata yet, waiting for onloadedmetadata.');
                      // Rely on onloadedmetadata to trigger checkAndPlay
                  }
              });
  
              console.log('视频播放和尺寸就绪，启动流媒体进程.');
              this.startStreamingProcesses();
  
          } catch (playError) {
              console.error('视频播放或尺寸就绪失败:', playError);
              let errorMessage = '视频播放失败';
              if (playError.name === 'NotAllowedError') {
                  errorMessage = '视频播放被阻止：需要用户交互或权限被拒绝。';
              } else if (playError.name === 'NotSupportedError') {
                  errorMessage = '视频格式或操作不被浏览器支持。';
              } else if (playError.name === 'AbortError') {
                  errorMessage = '视频播放被中断。';
              } else if (playError.message.includes('timed out')) {
                  errorMessage = '视频加载或播放超时。';
              } else {
                  errorMessage = '视频播放未知错误';
              }
              this.showToast(errorMessage + ': ' + playError.message);
              this.videoStatus = '失败';
              this.audioStatus = '失败';
              // Crucially, stop media if playback fails to avoid hanging resources
              this.stopAllMediaAndStreaming(); 
          }
          }
          // #endif

          // #ifndef H5
          // 非Web环境（如小程序）使用uni-app的API
          try {
            // 请求摄像头权限
            const cameraAuth = await uni.authorize({ scope: 'scope.camera' });
            console.log('摄像头权限已获取', cameraAuth);
            
            // 请求麦克风权限
            const audioAuth = await uni.authorize({ scope: 'scope.record' });
            console.log('麦克风权限已获取', audioAuth);
            
            // 模拟视频流和音频流
            this.videoStream = { active: true };
            this.audioStream = { active: true };
            
            // 模拟视频元素
            this.videoElement = { videoWidth: 640, videoHeight: 480 };
            
            this.videoStatus = '运行中';
            this.audioStatus = '运行中';
            console.log('[DEBUG] 小程序环境音视频状态已更新为运行中');
            
            // 在非Web环境也启动流媒体进程，包括WebSocket连接
            this.startStreamingProcesses();
            
            this.showToast('已准备就绪');
          } catch (authError) {
            console.error('权限请求失败:', authError);
            this.showToast('请授权摄像头和麦克风权限');
            this.videoStatus = '失败';
            this.audioStatus = '失败';
          }
          // #endif
  
        } catch (error) {
          console.error('媒体设备访问失败:', error);
          this.showToast('摄像头/麦克风权限被拒绝: ' + error.message);
          this.videoStatus = '失败';
          this.audioStatus = '失败';
        }
      },
  
      async startStreamingProcesses() { // EXISTING METHOD, now called after video play/dimensions are confirmed
          if (!this.videoStream || !this.audioStream || !this.videoElement) {
              console.error('视频、音频流或视频元素未就绪，无法启动流媒体功能.');
              this.showToast('流媒体启动失败：媒体或元素未就绪');
              return;
          }
          console.log('启动流媒体功能...');
  
          // 先设置状态为初始化中，避免显示失败状态
          this.videoStatus = '初始化中';
          this.audioStatus = '初始化中';
  
          // Setup video for streaming (Canvas and frame sending)
          this.setupVideoStreaming();
  
          // Setup audio processing
          this.setupAudioProcessing();
  
          // Connect WebSocket
          this.connectWebSocket();
      },
  
      // WebSocket连接建立后再启动帧发送，避免this.ws为空
      startFrameSending() {
          try {
              if (this.sendInterval) {
                  clearInterval(this.sendInterval);
                  this.sendInterval = null;
              }
              this.sendInterval = setInterval(() => {
                  this.captureAndSendFrame();
              }, 1000 / FPS);
              this.videoStatus = '运行中';
              console.log('[DEBUG] 帧发送定时器已启动');
          } catch (error) {
              console.error('[DEBUG] 启动帧发送定时器失败:', error);
          }
      },
  
      setupVideoStreaming() { 
          const video = this.videoElement;
          // At this point, we assume video is ready and playing due to await in startCamera
          console.log('设置视频流媒体：创建Canvas并启动帧发送.');
          
          // #ifdef H5
          // Web环境下使用document.createElement创建Canvas
          if (!this.canvas) {
              this.canvas = document.createElement('canvas');
          }
          this.canvas.width = video.videoWidth;
          this.canvas.height = video.videoHeight;
          
          // 安全获取Canvas上下文，避免可能的异常
          try {
          this.ctx = this.canvas.getContext('2d');
              console.log('[DEBUG] Web环境：成功获取Canvas上下文');
          } catch (h5CtxError) {
              console.error('[DEBUG] Web环境：获取Canvas上下文失败:', h5CtxError.message);
              this.ctx = null;
          }
  
          // 仅创建Canvas和上下文，不在此处启动帧发送；等待WebSocket连接成功后再启动
          if (this.sendInterval) {
              clearInterval(this.sendInterval);
              this.sendInterval = null;
          }
          this.videoStatus = '就绪';
          console.log('Canvas ready. Will start frame sending after WebSocket is open.');
          // #endif
          
          // #ifndef H5
          // 非Web环境（小程序）不使用document.createElement
          try {
              console.log('小程序环境：设置视频流媒体（使用小程序API）');
              // 在小程序环境中，不创建实际的Canvas元素
              // 而是直接设置视频状态并启动帧发送定时器
              // 视频尺寸信息可以通过video元素获取
              const videoWidth = video ? video.videoWidth || 640 : 640;
              const videoHeight = video ? video.videoHeight || 480 : 480;
              
              console.log('小程序环境：视频尺寸设置为', videoWidth, 'x', videoHeight);
              
              // 不在此处启动帧发送；等待WebSocket连接成功后再启动
              if (this.sendInterval) {
                  clearInterval(this.sendInterval);
                  this.sendInterval = null;
              }
              this.videoStatus = '就绪';
              console.log('小程序环境：视频就绪，等待WebSocket连接后再启动帧发送');
          } catch (error) {
              console.error('小程序环境：设置视频流媒体失败:', error);
              this.videoStatus = '失败';
              this.showToast('视频流媒体设置失败');
          }
          // #endif
      },
  
      async setupAudioProcessing() {
          if (!this.audioStream) {
              console.error('Audio stream is not available for setupAudioProcessing.');
              this.audioStatus = '失败';
              this.showToast('音频流不可用');
              return;
          }
          console.log('设置音频处理...');
          // #ifdef H5
          try {
            if (window && (window.AudioContext || window.webkitAudioContext)) {
          this.audioContext = new (window.AudioContext || window.webkitAudioContext)({
              sampleRate: AUDIO_SAMPLE_RATE
          });
  
          const source = this.audioContext.createMediaStreamSource(this.audioStream);
          // 使用更大的缓冲区大小，减少发送频率
          this.processor = this.audioContext.createScriptProcessor(4096, 1, 1);
  
          source.connect(this.processor);
          // 根据Whisper测试状态决定是否连接到扬声器
          if (this.whisperTestEnabled) {
          this.processor.connect(this.audioContext.destination);
            console.log('[DEBUG] 音频已连接到扬声器（Whisper测试模式）');
          } else {
            console.log('[DEBUG] 音频未连接到扬声器（Whisper测试未启用）');
          }
  
          this.processor.onaudioprocess = (e) => {
              if (this.ws && this.ws.readyState === WebSocket.OPEN) {
                  const audioData = e.inputBuffer.getChannelData(0);
                  const inputSampleRate = this.audioContext.sampleRate;
                  
                      // 更新音量显示
                      this.updateVolumeDisplay(audioData);
                  
                  // 降采样到16kHz（Whisper微服务要求）
                  const downsampledData = this.downsampleBuffer(audioData, inputSampleRate, 16000);
                  
                  // 转换为Int16 PCM格式
                  const pcmBytes = this.floatTo16BitPCM(downsampledData);
                  
                  // 确保数据格式正确
                  const correctedPcmBytes = this.ensureInt16Format(pcmBytes);
                  
                  // 累积并发送到Whisper微服务
                  this.maybeSendBytes(correctedPcmBytes);
                  
                  // 注意：实时播放通过processor直接连接到destination实现
              }
          };
          this.audioStatus = '运行中';
          console.log('[DEBUG] H5环境音频状态已更新为运行中');
            } else {
              console.warn('当前环境不支持Web Audio API');
              this.audioStatus = '不支持';
            }
          } catch (error) {
            console.error('设置音频处理失败:', error);
            this.audioStatus = '失败';
          }
          // #endif
          
          // #ifndef H5
          // 非Web环境（如小程序）：使用 RecorderManager 获取音频帧并通过WS发送
          try {
            if (!this.recorderManager) {
              this.recorderManager = uni.getRecorderManager();
              // 录音开始
              this.recorderManager.onStart(() => {
                console.log('[DEBUG] 录音已开始，录音管理器状态正常');
                this.audioStatus = '运行中';
              });
              // 录音错误
              this.recorderManager.onError((err) => {
                console.error('[DEBUG] 录音错误:', err);
                this.audioStatus = '失败';
                this.showToast('录音失败: ' + (err.errMsg || err.message || '未知错误'));
              });
              // 录音停止
              this.recorderManager.onStop((res) => {
                console.log('[DEBUG] 录音已停止:', res);
                this.audioStatus = '已停止';
              });
              // 帧回调（需设置 frameSize 才会触发）
              this.recorderManager.onFrameRecorded((res) => {
                try {
                  console.log('[DEBUG] 收到录音帧回调:', res);
                  const frameBuffer = res && (res.frameBuffer || res.buffer);
                  if (!frameBuffer) {
                    console.warn('[DEBUG] 录音帧数据为空');
                    return;
                  }
                  
                  //console.log('[DEBUG] 录音帧大小:', frameBuffer.byteLength || frameBuffer.length, 'bytes');
                  //console.log('[DEBUG] frameBuffer类型:', typeof frameBuffer, '构造函数:', frameBuffer.constructor.name);
                  
                  // 按照Whisper微服务期望：转换为Int16 PCM格式
                  let audioBytes;
                  
                  // 检查frameBuffer类型并转换为Int16 PCM
                  if (frameBuffer instanceof ArrayBuffer) {
                    // 如果是ArrayBuffer，需要检查是否为正确的Int16格式
                    const uint8Array = new Uint8Array(frameBuffer);
                    console.log('[DEBUG] 从ArrayBuffer转换，大小:', uint8Array.length);
                    
                    // 验证数据是否为Int16格式（每2个字节组成一个Int16样本）
                    if (uint8Array.length % 2 !== 0) {
                      //console.warn('[DEBUG] ArrayBuffer数据大小不是2的整数倍，跳过');
                      return;
                    }
                    
                    // 验证数据是否为有效的PCM数据
                    // 检查字节序是否正确（Int16 little-endian）
                    let isValidInt16 = true;
                    for (let i = 0; i < Math.min(20, uint8Array.length); i += 2) {
                      // 检查是否有明显的模式表明这不是Int16数据
                      if (uint8Array[i] === 0 && uint8Array[i + 1] === 0) {
                        // 连续的零值，可能是无效数据
                        if (i > 0 && uint8Array[i - 2] === 0 && uint8Array[i - 1] === 0) {
                          console.warn('[DEBUG] 检测到连续的零值，可能不是有效的PCM数据');
                          isValidInt16 = false;
                          break;
                        }
                      }
                    }
                    
                    if (!isValidInt16) {
                      console.warn('[DEBUG] 数据格式验证失败，跳过此帧');
                      return;
                    }
                    
                    audioBytes = uint8Array;
                  } else if (frameBuffer instanceof Uint8Array) {
                    // 如果已经是Uint8Array，直接使用
                    audioBytes = frameBuffer;
                    console.log('[DEBUG] 直接使用Uint8Array，大小:', audioBytes.length);
                  } else {
                    // 其他类型，尝试转换
                    audioBytes = new Uint8Array(frameBuffer);
                    console.log('[DEBUG] 从其他类型转换，大小:', audioBytes.length);
                  }
                  
                  // PCM格式需要确保数据大小是2的整数倍
                  console.log('[DEBUG] PCM音频格式数据，大小:', audioBytes.length);
                  
                  // 验证数据大小是否为Int16格式的整数倍
                  if (audioBytes.length % 2 !== 0) {
                    console.warn('[DEBUG] PCM音频数据大小不是Int16格式的整数倍:', audioBytes.length);
                    // 截断到最近的偶数
                    const correctedLength = Math.floor(audioBytes.length / 2) * 2;
                    audioBytes = audioBytes.slice(0, correctedLength);
                    console.log('[DEBUG] 修正后大小:', audioBytes.length);
                  }
                  
                  // 简单的数据有效性检查
                  let hasValidData = false;
                  for (let i = 0; i < Math.min(10, audioBytes.length); i++) {
                    if (audioBytes[i] !== 0) {
                      hasValidData = true;
                      break;
                    }
                  }
                  
                  if (!hasValidData) {
                    console.warn('[DEBUG] 音频数据可能无效（全零）');
                    return;
                  }
                  
                  console.log('[DEBUG] PCM音频数据验证通过');
                  
                  // 详细的数据格式检查
                  console.log('[DEBUG] 音频数据详细检查:');
                  console.log('  - 数据大小:', audioBytes.length, '字节');
                  console.log('  - 前10字节:', Array.from(audioBytes.slice(0, 10)));
                  console.log('  - 后10字节:', Array.from(audioBytes.slice(-10)));
                  console.log('[DEBUG] 音频数据有效性检查:', hasValidData, '前10字节:', Array.from(audioBytes.slice(0, 10)));
                  
                  // 更新音量显示（直接使用Uint8Array进行音量计算）
                  this.updateVolumeDisplay(audioBytes);
                  
                  // 播放发送给Whisper的音频（小程序环境，仅在测试模式下）
                  if (this.whisperTestEnabled) {
                    // 将Uint8Array转换为Float32Array进行播放
                    const floatAudioData = new Float32Array(audioBytes.length);
                    for (let i = 0; i < audioBytes.length; i++) {
                      floatAudioData[i] = (audioBytes[i] - 128) / 128; // 转换为-1到1的范围
                    }
                    this.playBackendAudio(floatAudioData);
                  }
                  
                  // AAC/MP3格式直接发送，不需要复杂的格式转换
                  // 微服务端会处理音频解码
                  this.maybeSendBytes(audioBytes);
                } catch (e) {
                  console.error('[DEBUG] 处理录音帧失败:', e);
                }
              });
            }
            // 启动录音：注意不同平台支持的参数不同
            // 采用平台允许范围的码率（WeChat: 24k~96k），并指定frameSize触发onFrameRecorded
            const startRecorder = (opts) => new Promise((resolve, reject) => {
              try {
                this.recorderManager.start(opts);
                resolve();
              } catch (err) {
                reject(err);
              }
            });

            try {
              // 直接使用PCM格式，小程序原生支持，最稳定
              console.log('[DEBUG] 尝试启动录音管理器...');
              await startRecorder({
                duration: 600000, // 10分钟
                sampleRate: 16000, // 16kHz采样率，符合Whisper期望
                numberOfChannels: 1, // 单声道
                format: 'PCM', // PCM格式，小程序原生支持
                frameSize: 8 // 8KB帧大小，确保是偶数，PCM格式合适
              });
              console.log('[DEBUG] 小程序录音管理器已启动 (PCM@16kHz, 8KB帧)');
            } catch (err1) {
              console.warn('[DEBUG] 录音启动失败(PCM)，准备降级重试 AAC@48kbps:', err1 && err1.message || err1);
              try {
                await startRecorder({
                  duration: 600000,
                  sampleRate: 16000,
                  numberOfChannels: 1,
                  encodeBitRate: 48000,
                  format: 'aac', // AAC格式，兼容性好
                  frameSize: 6 // 6KB帧大小，确保是偶数
                });
                console.log('[DEBUG] 小程序录音管理器已启动 (AAC@48kbps, 6KB帧)');
              } catch (err2) {
                console.warn('[DEBUG] 录音启动失败(AAC)，准备降级重试 MP3@32kbps:', err2 && err2.message || err2);
                try {
                  await startRecorder({
                    duration: 600000,
                    sampleRate: 16000,
                    numberOfChannels: 1,
                    encodeBitRate: 32000,
                    format: 'mp3', // MP3格式，兼容性最好
                    frameSize: 6 // 6KB帧大小，确保是偶数
                  });
                  console.log('[DEBUG] 小程序录音管理器已启动 (MP3@32kbps, 6KB帧)');
                } catch (err3) {
                  console.error('[DEBUG] 所有录音格式都启动失败:', err3 && err3.message || err3);
                  this.audioStatus = '失败';
                  this.showToast('录音启动失败，请检查麦克风权限');
                }
              }
            }
          } catch (e) {
            console.error('[DEBUG] 初始化录音管理器失败:', e);
            this.audioStatus = '失败';
          }
          // #endif
      },
      
      connectWebSocket() { // EXISTING METHOD
        // 检查考试状态，如果考试已结束或异常，不进行连接
        if (this.examEnded || this.examAbnormal) {
            console.log('[DEBUG] 考试已结束或异常，停止WebSocket连接');
            return;
        }
        
        const currentQid = this.currentQuestion?.id || '';
if (!currentQid) {
  console.warn('[DEBUG] 当前题目未就绪，延迟重连WS...');
  setTimeout(() => this.connectWebSocket(), 300);
  return;
}
          if (this.isConnected) {
              console.log('[DEBUG] WebSocket 已经连接，跳过重连');
              return;
          }

          // 直接连接Whisper微服务，不需要token
          const rawToken = uni.getStorageSync('accessToken') || '';
          const tokenForWs = encodeURIComponent(rawToken.replace(/^Bearer\s+/i, ''));
          const qs = [];
          qs.push(`accessToken=${tokenForWs}`);
          if (this.examTaskId) qs.push(`examTaskId=${encodeURIComponent(this.examTaskId)}`);
          if (currentQid) qs.push(`questionId=${encodeURIComponent(currentQid)}`);
          const wsUrlH5 = `wss://${NETWORK_CONFIG.API_BASE_URL}/api/ws-media?${qs.join('&')}`;
          console.log('[DEBUG] 准备连接后端媒体通道，URL:', wsUrlH5);

          // #ifdef H5
          // Web环境使用原生WebSocket API
          try {
          this.ws = new WebSocket(wsUrlH5);
          this.ws.binaryType = 'arraybuffer'; // 设置二进制类型
              console.log('[DEBUG] WebSocket对象已创建');
          } catch (error) {
              console.error('[DEBUG] 创建WebSocket对象失败:', error);
              this.showToast('创建WebSocket连接失败');
              return;
          }

          this.ws.onopen = () => {
              console.log('[DEBUG] 后端媒体通道连接已建立！readyState:', this.ws.readyState);
              this.isConnected = true;
              this.reconnectAttempts = 0;
              this.showToast('媒体通道连接已建立');
              
              // 确保音频状态正确显示（如果音频处理已设置但状态未更新）
              if (this.audioStream && this.audioStatus === '初始化中') {
                this.audioStatus = '运行中';
                console.log('[DEBUG] WebSocket连接建立后，确保音频状态为运行中');
              }
              
              // 连接建立后绑定当前题目与任务，避免握手期题目未就绪导致qid缺失
              try {
                const bindMsg = JSON.stringify({
                  type: 'bind',
                  examTaskId: this.examTaskId || null,
                  questionId: this.currentQuestion?.id || null
                });
                this.ws.send(bindMsg);
                console.log('[DEBUG] 已发送绑定消息:', bindMsg);
              } catch (e) {
                console.warn('[DEBUG] 绑定消息发送失败:', e);
              }
              // Whisper微服务不需要发送开始消息，直接开始音频采集
              // WebSocket建立后再启动帧发送
              if (!this.sendInterval) {
                  this.startFrameSending();
              }
          };

          this.ws.onmessage = (event) => {
              try {
                  // 后端返回JSON：{status, text}
                  if (typeof event.data === 'string') {
                      let recognizedText = '';
                      try {
                        const msg = JSON.parse(event.data);
                        
                        // 检查是否为防作弊消息
                        if (msg.type === 'anti_cheat_detected') {
                            console.log('[ANTI-CHEAT] 收到防作弊消息:', msg.message);
                            this.handleAntiCheatDetected(msg);
                            return;
                        }
                        
                        // 检查是否为异常断开消息
                        if (msg.type === 'abnormal_disconnect') {
                            console.log('[ABNORMAL] 收到异常断开消息:', msg.message);
                            this.handleAbnormalDisconnect(msg);
                            return;
                        }
                        
                        recognizedText = (msg && (msg.text || msg.message)) ? String(msg.text || msg.message) : event.data;
                      } catch (e) {
                        recognizedText = event.data.trim();
                      }
                      if (recognizedText) {
                        console.log('[DEBUG] 识别结果:', recognizedText);
                        this.appendRecognition(recognizedText);
                      }
                  } else {
                      // 二进制消息（通常不会收到）
                      console.log('[DEBUG] 收到二进制消息，长度:', event.data.byteLength);
                  }
              } catch (error) {
                  console.error('[DEBUG] 处理Whisper消息失败:', error);
              }
          };
  
          this.ws.onerror = (error) => {
              console.error('[DEBUG] WebSocket错误:', error.message || error);
              this.showToast('媒体通道连接发生错误');
              this.isConnected = false;
          };
  
          this.ws.onclose = (event) => {
              console.log('[DEBUG] WebSocket连接已关闭，代码:', event.code, '原因:', event.reason);
              this.isConnected = false;
  
              // 检查考试状态，如果考试已结束或异常，不进行重连
              if (this.examEnded || this.examAbnormal) {
                  console.log('[DEBUG] 考试已结束或异常，停止重连');
                  return;
              }
              
              // Only attempt reconnect if we had valid streams to begin with
              if (this.reconnectAttempts < this.maxReconnectAttempts && (this.videoStream || this.audioStream)) {
                  this.reconnectAttempts++;
                  console.log(`[DEBUG] 尝试第 ${this.reconnectAttempts} 次重连`);
                  setTimeout(() => {
                  this.connectWebSocket();
                  }, 3000);
              } else if (this.reconnectAttempts >= this.maxReconnectAttempts) {
                  console.log('[DEBUG] 达到最大重连次数，停止重连');
                  this.showToast('WebSocket连接失败，请刷新页面重试');
              }
          };
          // #endif
          
          // #ifndef H5
          // 非Web环境（如小程序）使用uni-app的WebSocket API
          try {
              const rawToken = uni.getStorageSync('accessToken') || '';
              const tokenForWs = encodeURIComponent((rawToken + '').replace(/^Bearer\s+/i, ''));
              const qs = [];
              qs.push(`accessToken=${tokenForWs}`);
              if (this.examTaskId) qs.push(`examTaskId=${encodeURIComponent(this.examTaskId)}`);
              const currentQid = this.currentQuestion?.id || '';
              if (currentQid) qs.push(`questionId=${encodeURIComponent(currentQid)}`);
              const wsUrlMp = `wss://${NETWORK_CONFIG.API_BASE_URL}/api/ws-media?${qs.join('&')}`;
              const socketTask = uni.connectSocket({
                  url: wsUrlMp,
                  success: () => {
                      console.log('[DEBUG] 小程序后端媒体通道连接已创建');
                  },
                  fail: (error) => {
                      console.error('[DEBUG] 创建小程序媒体通道连接失败:', error);
                      this.showToast('创建媒体通道连接失败');
                  }
              });
              
              // 立即保存socketTask到this.ws，而不是在success回调中
              this.ws = socketTask;
              console.log('[DEBUG] 小程序WebSocket对象已保存到this.ws');
              
              // 监听WebSocket连接打开事件
              socketTask.onOpen(() => {
                  console.log('[DEBUG] 小程序媒体通道连接已建立！');
                  this.isConnected = true;
                  this.reconnectAttempts = 0;
                  this.showToast('媒体通道连接已建立');
                  
                  // 确保音频状态正确显示（小程序环境）
                  if (this.audioStream && this.audioStatus === '初始化中') {
                    this.audioStatus = '运行中';
                    console.log('[DEBUG] 小程序WebSocket连接建立后，确保音频状态为运行中');
                  }
                  
                  // 连接建立后绑定当前题目与任务
                  try {
                    const bindMsg = JSON.stringify({
                      type: 'bind',
                      examTaskId: this.examTaskId || null,
                      questionId: this.currentQuestion?.id || null
                    });
                    this.ws.send({ data: bindMsg });
                    console.log('[DEBUG] 小程序已发送绑定消息:', bindMsg);
                  } catch (e) {
                    console.warn('[DEBUG] 小程序绑定消息发送失败:', e);
                  }
                  
                  // Whisper微服务不需要发送开始消息，直接开始音频采集
                  // WebSocket建立后再启动帧发送
                  if (!this.sendInterval) {
                      this.startFrameSending();
                  }
              });
              
              // 监听WebSocket接收到服务器的消息事件
              socketTask.onMessage((res) => {
                  try {
                      // 后端返回JSON：{status, text}
                      if (typeof res.data === 'string') {
                          let recognizedText = '';
                          try {
                            const msg = JSON.parse(res.data);
                            recognizedText = (msg && (msg.text || msg.message)) ? String(msg.text || msg.message) : res.data;
                          } catch (e) {
                            recognizedText = res.data.trim();
                          }
                          if (recognizedText) {
                              console.log('[DEBUG] 识别结果:', recognizedText);
                              this.appendRecognition(recognizedText);
                          }
                      } else {
                          console.log('[DEBUG] 收到二进制消息，长度:', res.data.byteLength || '未知');
                      }
                  } catch (error) {
                      console.error('[DEBUG] 处理Whisper消息失败:', error);
                  }
              });
              
              // 监听WebSocket错误事件
              socketTask.onError((error) => {
                  console.error('[DEBUG] 小程序媒体通道错误:', error);
                  this.showToast('媒体通道连接发生错误');
                  this.isConnected = false;
              });
              
              // 监听WebSocket关闭事件
              socketTask.onClose((res) => {
                  if (this.frameCounter % 5 === 0) {
                      console.log('[DEBUG] 小程序WebSocket连接已关闭，代码:', res.code, '原因:', res.reason);
                  }
                  this.isConnected = false;
                  
                  // 检查考试状态，如果考试已结束或异常，不进行重连
                  if (this.examEnded || this.examAbnormal) {
                      console.log('[DEBUG] 考试已结束或异常，停止重连');
                      return;
                  }
                  
                  // 重连逻辑
                  if (this.reconnectAttempts < this.maxReconnectAttempts && (this.videoStream || this.audioStream)) {
                      this.reconnectAttempts++;
                      console.log(`[DEBUG] 尝试第 ${this.reconnectAttempts} 次重连`);
                      setTimeout(() => {
                          this.connectWebSocket();
                      }, 3000);
                  } else if (this.reconnectAttempts >= this.maxReconnectAttempts) {
                      console.log('[DEBUG] 达到最大重连次数，停止重连');
                      this.showToast('WebSocket连接失败，请刷新页面重试');
                  }
              });
          } catch (error) {
              console.error('[DEBUG] 小程序WebSocket初始化异常:', error);
              this.showToast('WebSocket初始化失败');
          }
          // #endif
      },
  
      captureAndSendFrame() { 
          // 屏蔽视频帧捕获的调试日志，减少日志噪音
          // if (this.frameCounter % 5 === 0) {
          //     console.log('[DEBUG] 尝试捕获并发送视频帧...');
          //     console.log('[DEBUG] 状态检查 - videoElement:', !!this.videoElement, 'ws:', !!this.ws);
          // }
          
          if (!this.videoElement) {
              // 屏蔽视频帧捕获的调试日志
              // if (this.frameCounter % 5 === 0) {
              //     console.log('[DEBUG] 跳过帧发送: 视频元素不存在');
              // }
              return;
          }
          
          if (!this.ws) {
              // 屏蔽视频帧捕获的调试日志
              // if (this.frameCounter % 5 === 0) {
              //     console.log('[DEBUG] 跳过帧发送: WebSocket对象不存在，尝试重新连接...');
              // }
              // 强制重连，即使isConnected可能显示为true
              if (!this.reconnecting) {
                  this.reconnecting = true;
                  this.isConnected = false; // 重置连接状态，确保重连能够执行
                  this.connectWebSocket().finally(() => {
                      this.reconnecting = false;
                  });
              }
              return;
          }
          
          // #ifdef H5
          // Web环境下检查WebSocket连接状态
          if (this.ws.readyState !== WebSocket.OPEN) {
              // 屏蔽视频帧捕获的调试日志
              // console.log('[DEBUG] 跳过帧发送: WebSocket未连接，当前状态:', this.ws.readyState);
              return;
          }

          if (!this.videoElement.videoWidth || !this.videoElement.videoHeight) {
              // 屏蔽视频帧捕获的调试日志
              // console.log('[DEBUG] 视频未就绪（尺寸为零），跳过当前帧');
              return;
          }

          try {
              // 确保ctx存在后再调用drawImage方法
              if (!this.ctx) {
                  console.warn('[DEBUG] Canvas上下文不存在，跳过帧捕获');
                  return;
              }
              this.ctx.drawImage(this.videoElement, 0, 0, this.canvas.width, this.canvas.height);

              // 将Canvas内容转换为二进制数据
              this.canvas.toBlob((blob) => {
                  if (!blob) {
                      console.log('[DEBUG] Canvas转Blob失败');
                      return;
                  }
                  
                  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
                      console.log('[DEBUG] 跳过帧发送: WebSocket状态已变化');
                      return;
                  }
                  
                  // 屏蔽视频帧捕获的调试日志
                  // if (this.frameCounter % 5 === 0) {
                  // console.log('[DEBUG] Canvas生成Blob成功，大小:', (blob.size / 1024).toFixed(2), 'KB');
                  // }
                  
                      const reader = new FileReader();
                      reader.onload = (event) => {
                      try {
                          // 第一个字节为消息类型（0x01表示视频数据）
                          const messageType = new Uint8Array([0x01]);
                          // 创建包含消息类型和视频数据的完整数组
                          const videoData = new Uint8Array(event.target.result);
                          const combinedData = new Uint8Array(messageType.length + videoData.length);
                          combinedData.set(messageType);
                          
                          // 递增帧计数器，用于控制日志输出频率
                          this.frameCounter++;
                          combinedData.set(videoData, messageType.length);
                          
                          // 屏蔽视频帧捕获的调试日志
                          // if (this.frameCounter % 5 === 0) {
                          //     console.log('[DEBUG] 准备发送视频帧数据，总大小:', (combinedData.length / 1024).toFixed(2), 'KB');
                          // }
                          
                          // 发送二进制消息
                          this.ws.send(combinedData.buffer);
                          // 屏蔽视频帧捕获的调试日志
                          // if (this.frameCounter % 5 === 0) {
                          //     console.log('[DEBUG] 视频帧发送成功！');
                          // }
                      } catch (error) {
                          console.error('[DEBUG] 视频帧发送失败:', error);
                      }
                  };
                  reader.onerror = (error) => {
                      console.error('[DEBUG] FileReader读取Blob失败:', error);
                      };
                      reader.readAsArrayBuffer(blob);
              }, 'video/mp4', 0.7);
          } catch (error) {
              console.error('发送视频帧失败:', error);
          }
          // #endif
          
          // #ifndef H5
          // 非Web环境（小程序）使用uni-app的API
          try {
              // 在小程序环境中，WebSocket的状态检查和发送方式不同
              if (!this.ws) {
                  // 屏蔽视频帧捕获的调试日志
                  // console.log('[DEBUG] 小程序环境：WebSocket未连接');
                  return;
              }
              
              // 检测是否在开发者工具环境中运行
              const isDevTools = uni.getSystemSetting().platform === 'devtools';
              
              try {
                  // 1. 首先初始化Canvas上下文
                  if (!this.ctx || !this.canvas) {
                      // 创建一个临时canvas上下文
                      const query = uni.createSelectorQuery().in(this);
                      query.select('#videoCanvas')
                          .fields({ node: true, size: true })
                          .exec((res) => {
                              if (res && res[0] && res[0].node) {
                                  this.canvas = res[0].node;
                                  
                                  // 设置canvas尺寸
                                  this.canvas.width = this.canvasWidth || 640;
                                  this.canvas.height = this.canvasHeight || 480;
                                  
                                  // 增强的Canvas上下文获取逻辑 - 提供更健壮的兼容性处理
                                  try {
                                      // 1. 检查canvas对象的完整性和有效性
                                      if (!this.canvas || typeof this.canvas !== 'object') {
                                          console.error('[DEBUG] Canvas对象无效:', typeof this.canvas);
                                          this.sendMockVideoFrame();
                                          return;
                                      }
                                      
                                      // 2. 记录canvas对象的安全信息，完全避免访问可能导致问题的getContext属性
                                      // 屏蔽视频帧捕获的调试日志
                                      // if (this.frameCounter % 5 === 0) {
                                      //     try {
                                      //         console.log('[DEBUG] Canvas对象信息:', {
                                      //             canvasType: typeof this.canvas,
                                      //             isObject: this.canvas && typeof this.canvas === 'object',
                                      //             platform: uni.getSystemSetting().platform,
                                      //             SDKVersion: uni.getAppBaseInfo().SDKVersion
                                      //         });
                                      //     } catch (infoError) {
                                      //         console.error('[DEBUG] 记录Canvas信息时出错:', infoError.message);
                                      //     }
                                      // }
                                      
                                      // 3. 小程序环境下的优化策略：参考login页面实现，使用camera context获取真实视频帧
                                      try {
                                          // 1. 检查是否在开发者工具环境中运行
                                          if (isDevTools) {
                                              // 屏蔽视频帧捕获的调试日志
                                              // if (this.frameCounter % 5 === 0) {
                                              //     console.log('[DEBUG] 检测到开发者工具环境，使用模拟数据');
                                              // }
                                              this.sendMockVideoFrame();
                                          } else if (this.useLivePusher) {
                                              // 2. 优先使用live-pusher组件获取视频帧
                                              // 屏蔽视频帧捕获的调试日志
                                              // if (this.frameCounter % 5 === 0) {
                                              //     console.log('[DEBUG] 微信小程序环境：尝试使用live-pusher获取真实视频帧');
                                              // }
                                              this.captureFromLivePusher();
                                          } else {
                                              // 3. 备选方案：使用camera context获取真实摄像头画面
                                              const cameraContext = uni.createCameraContext();
                                              // 屏蔽视频帧捕获的调试日志
                                              // if (this.frameCounter % 5 === 0) {
                                              //     console.log('[DEBUG] 微信小程序环境：尝试使用camera context获取真实视频帧');
                                              // }
                                                
                                              // 使用takePhoto方法获取视频帧
                                              cameraContext.takePhoto({
                                                  quality: 'high',
                                                  success: (res) => {
                                                      if (res && res.tempImagePath) {
                                                          // 屏蔽视频帧捕获的调试日志
                                                          // if (this.frameCounter % 5 === 0) {
                                                          //     console.log('[DEBUG] 成功获取camera视频帧:', res.tempImagePath);
                                                          // }
                                                          // 压缩后再发送，避免消息过大导致断开
                                                          this.compressAndSend(res.tempImagePath);
                                                      } else {
                                                          // 屏蔽视频帧捕获的调试日志
                                                          // if (this.frameCounter % 5 === 0) {
                                                          //     console.warn('[DEBUG] 获取camera视频帧失败，将使用模拟数据');
                                                          // }
                                                          this.sendMockVideoFrame();
                                                      }
                                                  },
                                                  fail: (photoError) => {
                                                      // 屏蔽视频帧捕获的调试日志
                                                      // if (this.frameCounter % 5 === 0) {
                                                      //     console.error('[DEBUG] camera takePhoto失败:', photoError);
                                                      // }
                                                      // 失败时回退到模拟数据
                                                      this.sendMockVideoFrame();
                                                  }
                                              });
                                          }
                                           
                                          // 不再设置为null，保留canvas上下文以便重复使用
                                      } catch (anyError) {
                                          // 屏蔽视频帧捕获的调试日志
                                          // if (this.frameCounter % 5 === 0) {
                                          //     console.error('[DEBUG] 微信小程序环境处理异常:', anyError.message);
                                          // }
                                          this.ctx = null;
                                          // 异常时回退到模拟数据
                                          this.sendMockVideoFrame();
                                      }
                                      
                                      // 4. 无论上下文是否获取成功，都继续流程，但在后续步骤中进行检查
                              } catch (ctxError) {
                                  // 屏蔽视频帧捕获的调试日志
                                  // if (this.frameCounter % 5 === 0) {
                                  //     console.error('[DEBUG] 获取Canvas上下文时发生严重错误:', ctxError.message);
                                  // }
                                  // 不中断流程，继续执行，后续会有降级处理
                              }
                               
                              // 记录上下文获取状态，无论成功与否都继续流程
                              // 屏蔽视频帧捕获的调试日志
                              // if (this.frameCounter % 5 === 0) {
                              //     console.log('[DEBUG] Canvas上下文初始化完成，是否成功:', !!this.ctx);
                              // }
                              } else {
                                  // 屏蔽视频帧捕获的调试日志
                                  // if (this.frameCounter % 5 === 0) {
                                  //     console.error('[DEBUG] 无法获取canvas元素');
                                  // }
                                  // 如果无法获取canvas，直接使用模拟数据
                                  this.sendMockVideoFrame();
                              }
                          });
                      return;
                  }
                  
                  // 如果在开发者工具环境中运行，直接使用模拟数据
                  if (isDevTools) {
                      this.sendMockVideoFrame();
                      return;
                  }
                  
                  // 2. 从live-pusher组件获取视频帧
                  const livePusherContext = uni.createLivePusherContext('videoLivePusher', this);
                  
                  // 3. 尝试获取视频帧数据
                  try {
                      livePusherContext.snapshot({ 
                          success: (res) => {
                              if (this.frameCounter % 5 === 0) {
                                      console.log('[DEBUG] 小程序环境：成功获取视频截图:', res.tempImagePath);
                                  }
                              
                              // 4. 使用canvas处理截图 - 增加上下文有效性检查
                              try {
                                  // 再次检查上下文是否有效，防止在异步回调过程中丢失
                                  if (!this.ctx || typeof this.ctx.drawImage !== 'function') {
                                      console.error('[DEBUG] Canvas上下文无效或不支持drawImage方法');
                                      this.sendMockVideoFrame();
                                      return;
                                  }
                                  
                                  this.ctx.drawImage(res.tempImagePath, 0, 0, this.canvas.width, this.canvas.height);
                                  
                                  // 检查是否存在draw方法（小程序特有）
                                  if (typeof this.ctx.draw === 'function') {
                                      this.ctx.draw(false, () => {
                                        // 5. 将canvas内容转换为临时文件
                                        uni.canvasToTempFilePath({
                                          canvasId: 'videoCanvas',
                                          success: (res) => {
                                              if (this.frameCounter % 5 === 0) {
                                                  console.log('[DEBUG] 小程序环境：canvas转换为临时文件成功');
                                              }
                                                
                                              // 6. 读取文件数据
                                              uni.getFileSystemManager().readFile({
                                                  filePath: res.tempFilePath,
                                                  success: (res) => {
                                                      // 7. 第一个字节为消息类型（0x01表示视频数据）
                                                      const messageType = new Uint8Array([0x01]);
                                                      // 8. 创建包含消息类型和视频数据的完整数组
                                                      const videoData = new Uint8Array(res.data);
                                                      const combinedData = new Uint8Array(messageType.length + videoData.length);
                                                      combinedData.set(messageType);
                                                      combinedData.set(videoData, messageType.length);
                                                      
                                                      if (this.frameCounter % 5 === 0) {
                                                          console.log('[DEBUG] 准备发送视频帧数据，总大小:', (combinedData.length / 1024).toFixed(2), 'KB');
                                                      }
                                                      
                                                      // 9. 小程序环境下发送二进制数据
                                                      this.ws.send({
                                                          data: combinedData.buffer,
                                                          success: () => {
                                                              if (this.frameCounter % 5 === 0) {
                                                                  console.log('[DEBUG] 视频帧发送成功！');
                                                              }
                                                          },
                                                          fail: (error) => {
                                                              console.error('[DEBUG] 视频帧发送失败:', error);
                                                          }
                                                      });
                                                  },
                                                  fail: (error) => {
                                                      if (this.frameCounter % 5 === 0) {
                                                          console.error('[DEBUG] 读取文件数据失败:', error);
                                                      }
                                                      // 降级使用模拟数据
                                                      this.sendMockVideoFrame();
                                                  }
                                              });
                                          },
                                          fail: (error) => {
                                              console.error('[DEBUG] canvas转换为临时文件失败:', error);
                                              // 降级使用模拟数据
                                              this.sendMockVideoFrame();
                                          }
                                      });
                                  });
                              } else {
                                  // 如果没有draw方法，直接尝试转换为临时文件
                                  if (this.frameCounter % 5 === 0) {
                                      console.log('[DEBUG] Canvas上下文不支持draw方法，尝试直接转换');
                                  }
                                  // 不再处理这部分逻辑，统一到live-pusher处理
                                  this.sendMockVideoFrame();
                              }
                          } catch (drawError) {
                                  console.error('[DEBUG] canvas绘制失败:', drawError);
                                  // 降级使用模拟数据
                                  this.sendMockVideoFrame();
                              }
                          },
                          fail: (error) => {
                              console.error('[DEBUG] 获取视频截图失败:', error);
                              // 当无法获取真实视频帧时，降级使用模拟数据
                              this.sendMockVideoFrame();
                          }
                      });
                  } catch (snapshotError) {
                      console.error('[DEBUG] snapshot方法调用异常:', snapshotError);
                      // 降级使用模拟数据
                      this.sendMockVideoFrame();
                  }
              } catch (error) {
                  console.error('[DEBUG] 处理视频帧数据异常:', error);
                  // 降级使用模拟数据
                  this.sendMockVideoFrame();
              }
          } catch (error) {
              console.error('小程序环境发送视频帧失败:', error);
              // 降级使用模拟数据
              this.sendMockVideoFrame();
          }
          // #endif
      },
      
      // 发送模拟视频帧（作为降级策略）
      sendMockVideoFrame() {
          try {
              if (!this.ws) {
                  return;
              }
              
              // 第一个字节为消息类型（0x01表示视频数据）
              const messageType = new Uint8Array([0x01]);
              // 创建1KB的随机模拟视频数据
              const mockVideoData = new Uint8Array(1024);
              for (let i = 0; i < mockVideoData.length; i++) {
                  mockVideoData[i] = Math.floor(Math.random() * 256);
              }
              // 合并消息类型和视频数据
              const combinedData = new Uint8Array(messageType.length + mockVideoData.length);
              combinedData.set(messageType);
              combinedData.set(mockVideoData, messageType.length);
              
              // #ifndef H5
              // 小程序环境
              this.ws.send({
                  data: combinedData.buffer,
                  success: () => {
                      // 屏蔽视频帧捕获的调试日志
                      // if (this.frameCounter % 5 === 0) {
                      //     console.log('[DEBUG] 模拟视频帧发送成功！');
                      // }
                  },
                  fail: (error) => {
                      console.error('[DEBUG] 模拟视频帧发送失败:', error);
                  }
              });
              // #endif
              
              // #ifdef H5
              // Web环境
              this.ws.send(combinedData.buffer);
              // #endif
          } catch (error) {
              console.error('[DEBUG] 发送模拟视频帧异常:', error);
          }
      },
      
      // 将图片路径转换为二进制数据并发送
      async convertImageToBinaryAndSend(imagePath) {
          try {
              // 读取图片文件 - 使用Promise包装readFile以支持await
              const fileData = await new Promise((resolve, reject) => {
                  uni.getFileSystemManager().readFile({
                      filePath: imagePath,
                      success: (res) => resolve(res),
                      fail: (err) => reject(err)
                  });
              });
              
              if (!fileData || !fileData.data) {
                  console.error('[DEBUG] 读取图片文件失败，返回数据为空');
                  this.sendMockVideoFrame();
                  return;
              }
              
              // 兼容小程序：res.data 多为 ArrayBuffer，应使用 byteLength
              const byteLength = (fileData.data && typeof fileData.data.byteLength === 'number')
                  ? fileData.data.byteLength
                  : (fileData.data.length || 0);
              if (this.frameCounter % 5 === 0) {
                  //console.log('[DEBUG] 成功读取图片文件，文件大小:', (byteLength / 1024).toFixed(2), 'KB');
              }
              
              // 准备消息类型和数据
              const messageType = new Uint8Array([0x01]);
              // 小程序返回的是 ArrayBuffer，直接构造 Uint8Array；H5/其他若返回 TypedArray，则使用其 buffer
              let imageData;
              if (typeof fileData.data?.byteLength === 'number' && !(fileData.data instanceof Uint8Array)) {
                  imageData = new Uint8Array(fileData.data); // ArrayBuffer -> Uint8Array
              } else if (fileData.data?.buffer) {
                  imageData = new Uint8Array(fileData.data.buffer);
              } else {
                  // 兜底：尝试直接构造
                  imageData = new Uint8Array(fileData.data);
              }
              
              // 合并消息类型和图片数据
              const combinedData = new Uint8Array(messageType.length + imageData.length);
              combinedData.set(messageType);
              combinedData.set(imageData, messageType.length);
              
              if (this.frameCounter % 5 === 0) {
                  //console.log('[DEBUG] 准备发送真实视频帧数据，总大小:', (combinedData.length / 1024).toFixed(2), 'KB');
              }
              
              // 发送真实视频帧数据
              this.ws.send({
                  data: combinedData.buffer,
                  success: () => {
                      if (this.frameCounter % 5 === 0) {
                          //console.log('[DEBUG] 真实视频帧发送成功！');
                      }
                  },
                  fail: (error) => {
                      console.error('[DEBUG] 真实视频帧发送失败:', error);
                      // 发送失败时回退到模拟数据
                      this.sendMockVideoFrame();
                  }
              });
              
              // 清理临时文件（仅当是可删除的临时文件路径时）
              try {
                  if (typeof imagePath === 'string' && !imagePath.startsWith('http')) {
                      uni.getFileSystemManager().unlink({
                          filePath: imagePath,
                          fail: (e) => {
                              console.warn('[DEBUG] 清理临时文件失败:', e);
                          }
                      });
                  } else {
                      // http://tmp/ 在某些运行环境不可直接unlink，忽略
                      if (this.frameCounter % 5 === 0) {
                          console.log('[DEBUG] 跳过清理（不可删除的http临时路径）:', imagePath);
                      }
                  }
              } catch (cleanupError) {
                  console.warn('[DEBUG] 清理临时文件异常:', cleanupError);
              }
              
          } catch (error) {
              console.error('[DEBUG] 转换图片到二进制数据失败:', error);
              // 转换失败时回退到模拟数据
              this.sendMockVideoFrame();
          }
      },

      // 小程序端：压缩图片后再发送，降低单帧大小
      async compressAndSend(imagePath) {
          try {
              // 目标大小与质量可按需调整：quality 0-100
              const compressed = await new Promise((resolve, reject) => {
                  uni.compressImage({
                      src: imagePath,
                      quality: 60,
                      // H5不支持，MP-WEIXIN支持：可选指定宽高
                      success: (res) => resolve(res),
                      fail: (err) => reject(err)
                  });
              });
              const outPath = compressed?.tempFilePath || compressed?.apFilePath || imagePath;
              if (this.frameCounter % 5 === 0) {
                  //console.log('[DEBUG] 压缩完成，输出路径:', outPath);
              }
              await this.convertImageToBinaryAndSend(outPath);
          } catch (e) {
              console.error('[DEBUG] 压缩失败，回退直接发送原图:', e);
              await this.convertImageToBinaryAndSend(imagePath);
          }
      },

      sendAudioData(audioData) {
          if (!this.ws) {
              // 强制重连
              this.isConnected = false;
              this.connectWebSocket();
              return;
          }
          
          // #ifdef H5
          // Web环境下检查WebSocket连接状态
          if (this.ws.readyState !== WebSocket.OPEN) {
              console.log('[DEBUG] 跳过音频发送: WebSocket未连接，当前状态:', this.ws.readyState);
              return;
          }

          try {
              // 按照Whisper微服务期望：降采样到16kHz，转Int16 little-endian
              const TARGET_SAMPLE_RATE = 16000;
              const inputSampleRate = this.audioContext.sampleRate;
              
              // 降采样处理
              let processedData = audioData;
              if (inputSampleRate !== TARGET_SAMPLE_RATE) {
                  processedData = this.downsampleBuffer(audioData, inputSampleRate, TARGET_SAMPLE_RATE);
              }
              
              // 转换为Int16 little-endian格式
              const int16Array = new Int16Array(processedData.length);
              for (let i = 0; i < processedData.length; i++) {
                  const s = Math.max(-1, Math.min(1, processedData[i]));
                  int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
              }

              // 将新的音频数据追加到缓冲
              const audioBytes = new Uint8Array(int16Array.buffer);
              const merged = new Uint8Array(this.audioSendBuffer.length + audioBytes.length);
              merged.set(this.audioSendBuffer, 0);
              merged.set(audioBytes, this.audioSendBuffer.length);
              this.audioSendBuffer = merged;

              console.log('[DEBUG] H5音频缓冲大小:', this.audioSendBuffer.length, 'bytes');

              // 当缓冲达到32KB时发送（带0x02前缀给后端）
              const CHUNK_SIZE = 32000;
              while (this.audioSendBuffer.length >= CHUNK_SIZE) {
                  const chunk = this.audioSendBuffer.slice(0, CHUNK_SIZE);
                  this.audioSendBuffer = this.audioSendBuffer.slice(CHUNK_SIZE);
                  
                  // 添加0x02前缀，发送给后端
                  const messageType = new Uint8Array([0x02]);
                  const combinedData = new Uint8Array(messageType.length + chunk.length);
                  combinedData.set(messageType);
                  combinedData.set(chunk, messageType.length);
                  
                  console.log('[DEBUG] H5准备发送32KB音频数据（带0x02前缀），总大小:', combinedData.length, 'bytes');
                  this.ws.send(combinedData.buffer);
                  console.log('[DEBUG] H5音频块发送成功！');
              }
          } catch (error) {
              console.error('[DEBUG] 发送音频数据失败:', error);
          }
          // #endif
          
          // #ifndef H5
          // 非Web环境（小程序）使用uni-app的API
          try {
              // 模拟音频数据处理（实际应用中需要使用小程序提供的音频API）
              console.log('[DEBUG] 小程序环境，音频数据发送（模拟）');
              
              // 这里假设audioData是有效的音频数据
              // 实际应用中可能需要根据小程序的音频API调整数据处理方式
              try {
                  // 转换Float32Array到Int16Array (PCM格式)
              const int16Array = new Int16Array(audioData.length);
              for (let i = 0; i < audioData.length; i++) {
                  const s = Math.max(-1, Math.min(1, audioData[i]));
                  int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
              }

              // 第一个字节为消息类型（0x02表示音频数据）
              const messageType = new Uint8Array([0x02]);
              // 创建包含消息类型和音频数据的完整数组
              const audioBytes = new Uint8Array(int16Array.buffer);
              const combinedData = new Uint8Array(messageType.length + audioBytes.length);
              combinedData.set(messageType);
              combinedData.set(audioBytes, messageType.length);
              
                  console.log('[DEBUG] 准备发送音频数据，大小:', (combinedData.length / 1024).toFixed(2), 'KB');
                  
                  // 小程序环境下发送二进制数据
                  this.ws.send({
                      data: combinedData.buffer,
                      success: () => {
                          console.log('[DEBUG] 音频数据发送成功！');
                      },
                      fail: (error) => {
                          console.error('[DEBUG] 发送音频数据失败:', error);
                      }
                  });
          } catch (error) {
                  console.error('[DEBUG] 处理音频数据异常:', error);
          }
          } catch (error) {
              console.error('小程序环境发送音频数据失败:', error);
          }
          // #endif
      },
  
      toggleCamera() {
        this.videoFacingMode = this.videoFacingMode === 'user' ? 'environment' : 'user';
        
        // #ifdef H5
        // Web环境：停止所有媒体和流媒体，然后重启摄像头
        this.stopAllMediaAndStreaming();
        this.startCamera();
        // #endif
        
        // #ifndef H5
        // 小程序环境：切换camera摄像头
        try {
          const cameraContext = uni.createCameraContext();
          // 切换摄像头方向（前置/后置）
          cameraContext.switchCamera({
            success: () => {
              console.log('[DEBUG] 小程序环境：摄像头切换成功');
              this.showToast('摄像头已切换');
            },
            fail: (error) => {
              console.error('[DEBUG] 小程序环境：摄像头切换失败:', error);
              this.showToast('摄像头切换失败');
            }
          });
        } catch (error) {
          console.error('[DEBUG] 小程序环境：切换摄像头异常:', error);
          // 如果切换失败，则使用原始的重启方式
          this.stopAllMediaAndStreaming();
          this.startCamera();
        }
        // #endif
      },
  
      stopAllMediaAndStreaming() {
          console.log('停止所有媒体和流媒体功能...');
          
          
          // #ifdef H5
          // 先发送残留音频缓冲，再发送结束命令
          if (this.ws && this.ws.readyState === WebSocket.OPEN) {
              try {
                  if (this.whisperAudioBuffer && this.whisperAudioBuffer.length > 0) {
                      const messageType = new Uint8Array([0x02]);
                      const combinedData = new Uint8Array(messageType.length + this.whisperAudioBuffer.length);
                      combinedData.set(messageType);
                      combinedData.set(this.whisperAudioBuffer, messageType.length);
                      this.ws.send(combinedData.buffer);
                      console.log('[DEBUG] 发送残留音频缓冲: ' + this.whisperAudioBuffer.length + ' bytes');
                  }
                  console.log('Sending end recording command');
                  const endCommand = new Uint8Array([0x03]);
                  this.ws.send(endCommand.buffer);
              } catch (error) {
                  console.error('发送结束命令失败:', error);
              }
          }
  
          // Stop video stream
          if (this.videoStream) {
              this.videoStream.getTracks().forEach(track => track.stop());
              this.videoStream = null;
          }
          // Stop audio stream
          if (this.audioStream) {
              this.audioStream.getTracks().forEach(track => track.stop());
              this.audioStream = null;
          }
  
          // If video element exists, remove it from DOM
          if (this.videoElement && this.videoElement.parentNode) {
              this.videoElement.parentNode.removeChild(this.videoElement);
              this.videoElement = null;
          }
          
          // Clear interval
          if (this.sendInterval) {
              clearInterval(this.sendInterval);
              this.sendInterval = null;
          }
  
          // Close audio processing
          if (this.processor) {
              this.processor.disconnect();
              this.processor = null;
          }
  
          if (this.audioContext) {
              this.audioContext.close();
              this.audioContext = null;
          }
          
  
          // Close WebSocket
          if (this.ws) {
              this.isConnected = false;
              this.ws.close();
              this.ws = null;
          }

          // 清空音频发送缓冲
          this.audioSendBuffer = new Uint8Array(0);
          // 清空Whisper音频缓冲
          this.whisperAudioBuffer = new Uint8Array(0);
          
          // 重置音量显示
          this.resetVolumeDisplay();
          
          // 注：上方已在关闭前尽力发送残留缓冲
          // #endif
          
          // #ifndef H5
          // 非Web环境（小程序）的资源清理逻辑
          try {
              // 停止录音
              if (this.recorderManager) {
                  try {
                      this.recorderManager.stop();
                      console.log('[DEBUG] 已停止录音');
                  } catch (e) {
                      console.warn('[DEBUG] 停止录音失败:', e);
                  }
              }
              // 发送结束录制命令（如果WebSocket连接存在）
              if (this.ws) {
                  try {
                      console.log('小程序环境：发送结束录制命令');
                      // 发送二进制结束命令（0x03表示结束录制）
                      const endCommand = new Uint8Array([0x03]);
                      this.ws.send({
                          data: endCommand.buffer,
                          success: () => {
                              console.log('结束录制命令发送成功');
                          },
                          fail: (error) => {
                              console.error('发送结束命令失败:', error);
                          }
                      });
                  } catch (error) {
                      console.error('发送结束命令异常:', error);
                  }
              }
              
              // 清除定时器
              if (this.sendInterval) {
                  clearInterval(this.sendInterval);
                  this.sendInterval = null;
              }
              
              // 关闭WebSocket连接（小程序环境）
              if (this.ws) {
                  try {
                      console.log('小程序环境：关闭WebSocket连接');
                      this.isConnected = false;
                      // 使用uni-app的API关闭WebSocket连接
                      uni.closeSocket({
                          socketTaskId: this.ws.socketTaskId || this.ws._socketTaskId,
                          success: () => {
                              console.log('WebSocket连接已关闭');
                          },
                          fail: (error) => {
                              console.error('关闭WebSocket连接失败:', error);
                          }
                      });
                  } catch (error) {
                      console.error('关闭WebSocket连接异常:', error);
                  }
                  this.ws = null;
              }
              
              // 释放其他资源
              this.videoStream = null;
              this.audioStream = null;
              this.videoElement = null;
              this.audioContext = null;
              this.processor = null;
              console.log('小程序环境：所有媒体资源已释放');
          } catch (error) {
              console.error('小程序环境：释放资源失败:', error);
          }
          // #endif
  
          this.videoStatus = '已停止';
          this.audioStatus = '已停止';
      },
  
      showToast(message) { // EXISTING HELPER
        uni.showToast({
          title: message,
          icon: 'none',
          duration: 3000
        });
      },
      
      // Token刷新功能
      async refreshToken() {
        try {
          const refreshToken = uni.getStorageSync('refreshToken');
          if (!refreshToken) {
            console.log('[DEBUG] 没有refreshToken，跳过Token刷新');
            return null; // 直接返回，不报错
          }

          console.log('[DEBUG] 开始刷新Token...');
          const response = await uni.request({
            url: `${NETWORK_CONFIG.API_BASE_URL}/auth/refresh`,
            method: 'POST',
            header: {
              'Content-Type': 'application/json'
            },
            data: {
              refreshToken
            }
          });

          console.log('[DEBUG] Token刷新响应:', response.statusCode, response.data);

          if (response.statusCode === 200 && response.data && response.data.accessToken) {
            // 更新存储的token
            uni.setStorageSync('accessToken', response.data.accessToken);
            if (response.data.refreshToken) {
              uni.setStorageSync('refreshToken', response.data.refreshToken);
            }
            console.log('[DEBUG] Token刷新成功');
            return response.data.accessToken;
          } else {
            throw new Error('Invalid refresh response: ' + JSON.stringify(response.data));
          }
        } catch (error) {
          console.error('[DEBUG] Token刷新失败:', error);
          // 清除所有token
          uni.removeStorageSync('accessToken');
          uni.removeStorageSync('refreshToken');
          // 停止Token刷新定时器
          this.stopTokenRefreshInterval();
          // 显示提示并跳转到登录页
          this.showToast('登录已过期，请重新登录');
          setTimeout(() => {
            uni.redirectTo({
              url: '/pages/auth/login/index'
            });
          }, 2000);
          throw error;
        }
      },
      
      // 启动token自动刷新
      startTokenRefreshInterval() {
        // 清除可能存在的旧定时器
        if (this.refreshTokenTimer) {
          clearInterval(this.refreshTokenTimer);
        }

        // 检查是否有refreshToken
        const refreshToken = uni.getStorageSync('refreshToken');
        if (!refreshToken) {
          console.warn('[DEBUG] 没有refreshToken，跳过Token自动刷新');
          return;
        }

        console.log('[DEBUG] 启动Token自动刷新，每4分钟刷新一次');
        // 设置新的定时器，每4分钟刷新一次token
        this.refreshTokenTimer = setInterval(async () => {
          try {
            console.log('[DEBUG] 执行自动Token刷新...');
            const result = await this.refreshToken();
            if (result === null) {
              // 如果没有refreshToken，停止定时器
              console.log('[DEBUG] 没有refreshToken，停止自动刷新');
              this.stopTokenRefreshInterval();
            }
          } catch (error) {
            console.error('[DEBUG] 自动Token刷新失败:', error);
            // 如果自动刷新失败，清除定时器
            this.stopTokenRefreshInterval();
          }
        }, 4 * 60 * 1000); // 4分钟
      },
      
      // 停止token自动刷新
      stopTokenRefreshInterval() {
        if (this.refreshTokenTimer) {
          clearInterval(this.refreshTokenTimer);
          this.refreshTokenTimer = null;
        }
      },
      
      // 检查考试状态
      async checkExamStatus() {
        try {
          if (!this.examTaskId || this.examEnded || this.examAbnormal) {
            return;
          }
          
          console.log('[DEBUG] 检查考试状态，examTaskId:', this.examTaskId);
          const response = await uni.request({
            url: `${NETWORK_CONFIG.API_BASE_URL}/exam/task/detail?examTaskId=${this.examTaskId}`,
            method: 'GET',
            header: {
              'Access-Token': `Bearer ${uni.getStorageSync('accessToken')}`
            }
          });
          
          if (response.statusCode === 200 && response.data) {
            const examTask = response.data.examTask || response.data;
            if (examTask.status) {
              console.log('[DEBUG] 考试状态检查结果:', examTask.status);
              if (examTask.status === 4) {
                // 考试任务已异常结束
                console.log('[DEBUG] 检测到考试任务已异常结束，停止所有活动');
                this.examAbnormal = true;
                this.examEnded = true;
                this.stopAllMediaAndStreaming();
                this.disconnectAllExamWebSockets();
                this.showExamCompleteModal = true;
                this.showExamContent = false;
                this.showToast('考试已异常结束');
                return;
              } else if (examTask.status === 3) {
                // 考试任务已完成
                console.log('[DEBUG] 检测到考试任务已完成，停止所有活动');
                this.examEnded = true;
                this.stopAllMediaAndStreaming();
                this.disconnectAllExamWebSockets();
                this.showExamCompleteModal = true;
                this.showExamContent = false;
                this.showToast('考试已完成');
                return;
              }
            }
          }
        } catch (error) {
          console.error('[DEBUG] 检查考试状态失败:', error);
        }
      },
      
      // 启动考试状态检查定时器
      startExamStatusCheck() {
        if (this.examStatusCheckTimer) {
          clearInterval(this.examStatusCheckTimer);
        }
        
        console.log('[DEBUG] 启动考试状态检查定时器，每10秒检查一次');
        this.examStatusCheckTimer = setInterval(() => {
          this.checkExamStatus();
        }, 10000); // 每10秒检查一次
      },
      
      // 停止考试状态检查定时器
      stopExamStatusCheck() {
        if (this.examStatusCheckTimer) {
          clearInterval(this.examStatusCheckTimer);
          this.examStatusCheckTimer = null;
        }
      },
      
      // 标记页面已离开（防作弊）
      markPageLeft() {
        try {
          const userInfo = uni.getStorageSync('userInfo');
          if (!userInfo || !userInfo.id) {
            return;
          }
          
          const examSessionKey = `exam_session_${userInfo.id}`;
          const examSessionData = uni.getStorageSync(examSessionKey) || {};
          
          // 只有在考试已开始的情况下才标记为离开
          if (examSessionData.examStarted) {
            examSessionData.hasLeftPage = true;
            examSessionData.pageLeaveTime = Date.now();
            
            uni.setStorageSync(examSessionKey, examSessionData);
            console.log('[ANTI-CHEAT] 页面已标记为离开，考试状态:', examSessionData);
            
            // 设置前端状态，防止重连
            this.examAbnormal = true;
            this.examEnded = true;
          }
          
        } catch (error) {
          console.error('[ANTI-CHEAT] 标记页面离开失败:', error);
        }
      },
      
      // 处理防作弊检测消息
      handleAntiCheatDetected(data) {
        try {
          console.log('[ANTI-CHEAT] 处理防作弊检测消息:', data);
          
          // 设置考试异常结束状态，停止重连
          this.examAbnormal = true;
          this.examEnded = true;
          
          // 停止所有媒体流
          this.stopAllMediaAndStreaming();
          
          // 主动断开所有WebSocket连接
          this.disconnectAllExamWebSockets();
          
          // 显示防作弊提示
          uni.showModal({
            title: '防作弊检测',
            content: data.message || '检测到重连，考试已结束，所有答案已清空。',
            showCancel: false,
            confirmText: '确定',
            success: () => {
              // 显示考试完成模态框
              this.showExamCompleteModal = true;
              this.showExamContent = false;
              
              // 清理本地存储
              const userInfo = uni.getStorageSync('userInfo');
              if (userInfo && userInfo.id) {
                const examSessionKey = `exam_session_${userInfo.id}`;
                uni.removeStorageSync(examSessionKey);
              }
            }
          });
          
        } catch (error) {
          console.error('[ANTI-CHEAT] 处理防作弊消息失败:', error);
        }
      },
      
      handleAbnormalDisconnect(data) {
        try {
          console.log('[ABNORMAL] 处理异常断开消息:', data);
          
          // 设置考试异常结束状态，停止重连
          this.examAbnormal = true;
          this.examEnded = true;
          
          // 停止所有媒体流
          this.stopAllMediaAndStreaming();
          
          // 主动断开所有WebSocket连接
          this.disconnectAllExamWebSockets();
          
          // 显示异常断开提示
          uni.showModal({
            title: '考试异常结束',
            content: data.message || '检测到异常，考试已结束。',
            showCancel: false,
            confirmText: '确定',
            success: () => {
              // 显示考试完成模态框
              this.showExamCompleteModal = true;
              this.showExamContent = false;
              
              // 清理本地存储
              const userInfo = uni.getStorageSync('userInfo');
              if (userInfo && userInfo.id) {
                const examSessionKey = `exam_session_${userInfo.id}`;
                uni.removeStorageSync(examSessionKey);
              }
            }
          });
          
        } catch (error) {
          console.error('[ABNORMAL] 处理异常断开消息失败:', error);
        }
      },
      
      disconnectAllExamWebSockets() {
        try {
          console.log('[ABNORMAL] 开始主动断开所有考试WebSocket连接');
          
          // 1. 断开主WebSocket连接
          if (this.ws) {
            try {
              this.ws.close();
              console.log('[ABNORMAL] 已主动关闭主WebSocket连接');
            } catch (e) {
              console.error('[ABNORMAL] 关闭主WebSocket连接失败:', e);
            }
            this.ws = null;
          }
          
          // 2. 断开小程序WebSocket连接
          if (this.socketTask) {
            try {
              this.socketTask.close();
              console.log('[ABNORMAL] 已主动关闭小程序WebSocket连接');
            } catch (e) {
              console.error('[ABNORMAL] 关闭小程序WebSocket连接失败:', e);
            }
            this.socketTask = null;
          }
          
          // 3. 重置连接状态
          this.isConnected = false;
          this.reconnectAttempts = 0;
          
          // 4. 停止所有定时器
          if (this.sendInterval) {
            clearInterval(this.sendInterval);
            this.sendInterval = null;
          }
          
          console.log('[ABNORMAL] 所有考试WebSocket连接已主动断开');
          
        } catch (error) {
          console.error('[ABNORMAL] 主动断开WebSocket连接失败:', error);
        }
      },
      
      // 主动清理WebSocket会话缓存
      async cleanupWebSocketSession() {
        try {
          // 获取当前用户ID
          const userInfo = uni.getStorageSync('userInfo');
          if (!userInfo || !userInfo.id) {
            console.log('[DEBUG] 没有用户信息，跳过WebSocket缓存清理');
            return;
          }
          
          console.log('[DEBUG] 开始清理WebSocket会话缓存，用户ID:', userInfo.id);
          
          // 调用后端清理接口
          const response = await uni.request({
            url: `${NETWORK_CONFIG.API_BASE_URL}/exam/cleanup-session?userId=${userInfo.id}`,
            method: 'POST',
            header: {
              'Access-Token': `Bearer ${uni.getStorageSync('accessToken')}`
            }
          });
          
          if (response.statusCode === 200) {
            console.log('[DEBUG] WebSocket会话缓存清理成功:', response.data);
          } else {
            console.warn('[DEBUG] WebSocket会话缓存清理失败:', response.data);
          }
        } catch (error) {
          console.error('[DEBUG] 清理WebSocket会话缓存异常:', error);
          // 清理失败不影响用户体验，只记录日志
        }
      }
    },
    mounted() {
      this.initPage();
      // 启动Token自动刷新
      this.startTokenRefreshInterval();
    },
    beforeDestroy() {
      this.stopAllMediaAndStreaming();
      // 清除计时器
      if (this.questionTimer) {
        clearInterval(this.questionTimer);
        this.questionTimer = null;
      }
      // 停止Token自动刷新
      this.stopTokenRefreshInterval();
      // 停止考试状态检查
      this.stopExamStatusCheck();
      // 主动清理WebSocket缓存
      this.cleanupWebSocketSession();
    },
    
    // 页面隐藏时也清理（适用于小程序）
    onHide() {
      this.markPageLeft();
      this.cleanupWebSocketSession();
    },
    
    // 页面卸载时清理（适用于H5）
    onUnload() {
      this.markPageLeft();
      this.cleanupWebSocketSession();
    }
  };
  </script>
  
  <style scoped>
  .container {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 20px;
    background-color: #f5f5f5; /* ADDED from user's reference style */
    min-height: 100vh; /* ADDED from user's reference style */
  }
  
  /* 模态框样式 */
  .modal {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0, 0, 0, 0.5);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 1000;
  }
  
  .modal-content {
    background-color: white;
    border-radius: 10px;
    padding: 20px;
    max-width: 500px;
    width: 90%;
    max-height: 80vh;
    overflow-y: auto;
  }
  
  .modal-content h2 {
    color: #333;
    margin-bottom: 16px;
    text-align: center;
  }
  
  .modal-content p {
    color: #666;
    line-height: 1.6;
    margin-bottom: 20px;
  }
  
  /* 复选框容器 */
  .checkbox-container {
    display: flex;
    align-items: center;
    margin: 20px 0;
  }
  
  .checkbox-container checkbox {
    margin-right: 10px;
  }
  
  /* 按钮样式 */
  .start-btn, .answer-btn, .complete-btn {
    width: 100%;
    padding: 15px;
    background-color: #007aff;
    color: white;
    border: none;
    border-radius: 8px;
    font-size: 16px;
    font-weight: bold;
    cursor: pointer;
    margin-top: 20px;
  }
  
  .start-btn:disabled {
    background-color: #cccccc;
    cursor: not-allowed;
  }
  
  .answer-btn {
    background-color: #5ac8fa;
    margin-top: 20px;
  }
  
  .complete-btn {
    background-color: #34c759;
  }
  
  .action-btn {
    padding: 12px 24px;
    background-color: #007aff;
    color: white;
    border: none;
    border-radius: 8px;
    font-size: 16px;
    margin: 10px 0;
    cursor: pointer;
  }
  
  /* 计时器样式 */
  .timer {
    position: fixed;
    top: 100px;
    right: 20px;
    background-color: #ff3b30;
    color: white;
    padding: 10px 20px;
    border-radius: 20px;
    font-weight: bold;
    z-index: 100;
  }
  
  /* 考试内容区域 */
  .exam-content {
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 100%;
  }
  
  /* 问题容器 */
  .question-container {
    background-color: white;
    border-radius: 10px;
    padding: 20px;
    margin: 20px 0;
    width: 100%;
    max-width: 640px;
  }
  
  .question-container h3 {
    color: #333;
    margin-bottom: 15px;
  }
  
  .question-text {
    color: #666;
    line-height: 1.6;
    font-size: 16px;
  }
  
  /* 视频预览样式 */
  .video-preview {
    width: 100%;
    max-width: 640px;
    height: 480px;
    background-color: #000;
    border-radius: 8px;
    margin-bottom: 20px;
  }
  
  /* 状态信息 */
  .status {
    margin-top: 20px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    color: #666;
    font-size: 14px;
  }
  
  /* 音量显示 */
  .volume-display {
    margin-top: 20px;
    padding: 15px;
    background-color: white;
    border-radius: 10px;
    width: 100%;
    max-width: 640px;
  }

  /* 识别结果面板 */
  .recognition-panel {
    margin-top: 16px;
    padding: 12px;
    background-color: white;
    border-radius: 10px;
    width: 100%;
    max-width: 640px;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  .recognition-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .recognition-title {
    font-size: 16px;
    font-weight: bold;
    color: #333;
  }
  .recognition-body {
    max-height: 220px; /* 可滚动区域高度 */
    overflow-y: auto;
    background: #fafafa;
    border: 1px solid #eee;
    border-radius: 8px;
    padding: 10px;
  }
  .recognition-text {
    margin: 0;
    white-space: pre-wrap; /* 保留换行并自动换行 */
    word-break: break-word; /* 长词换行 */
    font-size: 14px;
    color: #444;
    line-height: 1.6;
  }
  
  .volume-label {
    font-size: 16px;
    font-weight: bold;
    color: #333;
    margin-bottom: 10px;
    text-align: center;
  }
  
  .volume-bar-container {
    width: 100%;
    height: 20px;
    background-color: #f0f0f0;
    border-radius: 10px;
    overflow: hidden;
    margin-bottom: 10px;
  }
  
  .volume-bar {
    height: 100%;
    background: linear-gradient(90deg, #4CAF50 0%, #8BC34A 50%, #FFC107 70%, #FF5722 100%);
    border-radius: 10px;
    transition: width 0.1s ease;
  }
  
  .volume-text {
    text-align: center;
    font-size: 18px;
    font-weight: bold;
    color: #333;
  }
  
  .volume-info {
    display: flex;
    justify-content: space-between;
    margin-top: 8px;
    padding: 0 10px;
  }
  
  .volume-info-text {
    font-size: 12px;
    color: #666;
    background-color: #f0f0f0;
    padding: 2px 6px;
    border-radius: 4px;
  }
  
  
  /* 视频容器样式 */
  .video-container {
    width: 100%;
    height: 300px;
    border-radius: 12px;
    overflow: hidden;
    background: #f0f0f0;
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 10px 0;
  }
  
  /* 视频元素样式 */
  .video-preview {
    max-width: 100%;
    max-height: 100%;
    object-fit: cover;
  }
  
  /* 考试结果模态框样式 */
  .exam-result-modal {
    max-width: 90%;
    max-height: 90vh;
    overflow-y: auto;
  }
  
  .loading-container {
    text-align: center;
    padding: 40px 20px;
    color: #666;
  }
  
  .result-container {
    padding: 0 10px;
  }
  
  .score-section {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    padding: 20px;
    border-radius: 12px;
    margin-bottom: 20px;
    text-align: center;
  }
  
  .total-score {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-bottom: 10px;
  }
  
  .score-label {
    font-size: 16px;
    opacity: 0.9;
    margin-bottom: 5px;
  }
  
  .score-value {
    font-size: 36px;
    font-weight: bold;
  }
  
  .score-details {
    font-size: 14px;
    opacity: 0.8;
  }
  
  .questions-section {
    margin-bottom: 20px;
  }
  
  .questions-section h3 {
    color: #333;
    margin-bottom: 15px;
    font-size: 18px;
  }
  
  .question-item {
    background: white;
    border-radius: 8px;
    padding: 15px;
    margin-bottom: 12px;
    border-left: 4px solid #007aff;
  }
  
  .question-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 10px;
  }
  
  .question-number {
    font-weight: bold;
    color: #333;
  }
  
  .question-score {
    background: #007aff;
    color: white;
    padding: 4px 12px;
    border-radius: 12px;
    font-size: 14px;
    font-weight: bold;
  }
  
  .question-content {
    margin-bottom: 10px;
  }
  
  .question-text {
    color: #666;
    line-height: 1.5;
  }
  
  .model-answer {
    background: #f8f9fa;
    padding: 10px;
    border-radius: 6px;
    border-left: 3px solid #28a745;
  }
  
  .answer-label {
    font-weight: bold;
    color: #28a745;
    margin-right: 5px;
  }
  
  .answer-text {
    color: #555;
    line-height: 1.4;
  }
  
  .badges-section {
    margin-bottom: 20px;
  }
  
  .badges-section h3 {
    color: #333;
    margin-bottom: 15px;
    font-size: 18px;
  }
  
  .badge-item {
    background: white;
    border-radius: 8px;
    padding: 15px;
    margin-bottom: 12px;
    border: 2px solid #e9ecef;
    cursor: pointer;
    transition: all 0.3s ease;
  }
  
  .badge-item:hover {
    border-color: #007aff;
    box-shadow: 0 2px 8px rgba(0, 122, 255, 0.2);
  }
  
  .badge-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 8px;
  }
  
  .badge-name {
    font-weight: bold;
    color: #333;
    font-size: 16px;
  }
  
  .badge-level {
    background: #ffc107;
    color: #333;
    padding: 2px 8px;
    border-radius: 10px;
    font-size: 12px;
    font-weight: bold;
  }
  
  .badge-evaluation {
    color: #666;
    font-size: 14px;
    margin-bottom: 5px;
  }
  
  .badge-score {
    color: #007aff;
    font-size: 14px;
    font-weight: bold;
  }
  
  .abnormal-container {
    text-align: center;
    padding: 40px 20px;
  }
  
  .abnormal-text {
    color: #dc3545;
    font-size: 18px;
    font-weight: bold;
    display: block;
    margin-bottom: 10px;
  }
  
  .abnormal-desc {
    color: #666;
    font-size: 14px;
  }
  
  .default-container {
    text-align: center;
    padding: 40px 20px;
    color: #666;
  }
  
  </style>